Initial commit with translated description
This commit is contained in:
176
SKILL.md
Normal file
176
SKILL.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
---
|
||||||
|
name: figma
|
||||||
|
description: "专业Figma设计分析和资产导出。"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Figma Design Analysis & Export
|
||||||
|
|
||||||
|
Professional-grade Figma integration for design system analysis, asset export, and comprehensive design auditing.
|
||||||
|
|
||||||
|
## Core Capabilities
|
||||||
|
|
||||||
|
### 1. File Operations & Analysis
|
||||||
|
- **File inspection**: Get complete JSON representation of any Figma file
|
||||||
|
- **Component extraction**: List all components, styles, and design tokens
|
||||||
|
- **Asset export**: Batch export frames, components, or specific nodes as PNG/SVG/PDF
|
||||||
|
- **Version management**: Access specific file versions and branch information
|
||||||
|
|
||||||
|
**Example usage:**
|
||||||
|
- "Export all components from this design system file"
|
||||||
|
- "Get the JSON data for these specific frames"
|
||||||
|
- "Show me all the colors and typography used in this file"
|
||||||
|
|
||||||
|
### 2. Design System Management
|
||||||
|
- **Style auditing**: Analyze color usage, typography consistency, spacing patterns
|
||||||
|
- **Component analysis**: Identify unused components, measure usage patterns
|
||||||
|
- **Brand compliance**: Check adherence to brand guidelines across files
|
||||||
|
- **Design token extraction**: Generate CSS/JSON design tokens from Figma styles
|
||||||
|
|
||||||
|
**Example usage:**
|
||||||
|
- "Audit this design system for accessibility issues"
|
||||||
|
- "Generate CSS custom properties from these Figma styles"
|
||||||
|
- "Find all inconsistencies in our component library"
|
||||||
|
|
||||||
|
### 3. Bulk Asset Export
|
||||||
|
- **Multi-format exports**: Export assets as PNG, SVG, PDF, or WEBP
|
||||||
|
- **Platform-specific sizing**: Generate @1x, @2x, @3x assets for iOS/Android
|
||||||
|
- **Organized output**: Automatic folder organization by format or platform
|
||||||
|
- **Client packages**: Complete deliverable packages with documentation
|
||||||
|
|
||||||
|
**Example usage:**
|
||||||
|
- "Export all components in PNG and SVG formats"
|
||||||
|
- "Generate complete asset package for mobile app development"
|
||||||
|
- "Create client deliverable with all marketing assets"
|
||||||
|
|
||||||
|
### 4. Accessibility & Quality Analysis
|
||||||
|
- **Contrast checking**: Verify WCAG color contrast requirements
|
||||||
|
- **Font size analysis**: Ensure readable typography scales
|
||||||
|
- **Interactive element sizing**: Check touch target requirements
|
||||||
|
- **Focus state validation**: Verify keyboard navigation patterns
|
||||||
|
|
||||||
|
**Example usage:**
|
||||||
|
- "Check this design for WCAG AA compliance"
|
||||||
|
- "Analyze touch targets for mobile usability"
|
||||||
|
- "Generate an accessibility report for this app design"
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Authentication Setup
|
||||||
|
```bash
|
||||||
|
# Set your Figma access token
|
||||||
|
export FIGMA_ACCESS_TOKEN="your-token-here"
|
||||||
|
|
||||||
|
# Or store in .env file
|
||||||
|
echo "FIGMA_ACCESS_TOKEN=your-token" >> .env
|
||||||
|
```
|
||||||
|
|
||||||
|
### Basic Operations
|
||||||
|
```bash
|
||||||
|
# Get file information and structure
|
||||||
|
python scripts/figma_client.py get-file "your-file-key"
|
||||||
|
|
||||||
|
# Export frames as images
|
||||||
|
python scripts/export_manager.py export-frames "file-key" --formats png,svg
|
||||||
|
|
||||||
|
# Analyze design system consistency
|
||||||
|
python scripts/style_auditor.py audit-file "file-key" --generate-html
|
||||||
|
|
||||||
|
# Check accessibility compliance
|
||||||
|
python scripts/accessibility_checker.py "file-key" --level AA --format html
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow Patterns
|
||||||
|
|
||||||
|
### Design System Audit Workflow
|
||||||
|
1. **Extract file data** → Get components, styles, and structure
|
||||||
|
2. **Analyze consistency** → Check for style variations and unused elements
|
||||||
|
3. **Generate report** → Create detailed findings and recommendations
|
||||||
|
4. **Manual implementation** → Use findings to guide design improvements
|
||||||
|
|
||||||
|
### Asset Export Workflow
|
||||||
|
1. **Identify export targets** → Specify frames, components, or nodes
|
||||||
|
2. **Configure export settings** → Set formats, sizes, and naming conventions
|
||||||
|
3. **Batch process** → Export multiple assets simultaneously
|
||||||
|
4. **Organize output** → Structure files for handoff or implementation
|
||||||
|
|
||||||
|
### Analysis & Documentation Workflow
|
||||||
|
1. **Extract design data** → Pull components, styles, and design tokens
|
||||||
|
2. **Audit compliance** → Check accessibility and brand consistency
|
||||||
|
3. **Generate documentation** → Create style guides and component specs
|
||||||
|
4. **Export deliverables** → Package assets for development or client handoff
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
### scripts/
|
||||||
|
- `figma_client.py` - Complete Figma API wrapper with all REST endpoints
|
||||||
|
- `export_manager.py` - Professional asset export with multiple formats and scales
|
||||||
|
- `style_auditor.py` - Design system analysis and brand consistency checking
|
||||||
|
- `accessibility_checker.py` - Comprehensive WCAG compliance validation and reporting
|
||||||
|
|
||||||
|
### references/
|
||||||
|
- `figma-api-reference.md` - Complete API documentation and examples
|
||||||
|
- `design-patterns.md` - UI patterns and component best practices
|
||||||
|
- `accessibility-guidelines.md` - WCAG compliance requirements
|
||||||
|
- `export-formats.md` - Asset export options and specifications
|
||||||
|
|
||||||
|
### assets/
|
||||||
|
- `templates/design-system/` - Pre-built component library templates
|
||||||
|
- `templates/brand-kits/` - Standard brand guideline structures
|
||||||
|
- `templates/wireframes/` - Common layout patterns and flows
|
||||||
|
|
||||||
|
## Integration Examples
|
||||||
|
|
||||||
|
### With Development Workflows
|
||||||
|
```bash
|
||||||
|
# Generate design tokens for CSS
|
||||||
|
python scripts/export_manager.py export-tokens "file-key" --format css
|
||||||
|
|
||||||
|
# Create component documentation
|
||||||
|
python scripts/figma_client.py document-components "file-key" --output docs/
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Brand Management
|
||||||
|
```bash
|
||||||
|
# Audit brand compliance in designs
|
||||||
|
python scripts/style_auditor.py audit-file "file-key" --brand-colors "#FF0000,#00FF00,#0000FF"
|
||||||
|
|
||||||
|
# Extract current brand colors for analysis
|
||||||
|
python scripts/figma_client.py extract-colors "file-key" --output brand-colors.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Client Deliverables
|
||||||
|
```bash
|
||||||
|
# Generate client presentation assets
|
||||||
|
python scripts/export_manager.py client-package "file-key" --template presentation
|
||||||
|
|
||||||
|
# Create development handoff assets
|
||||||
|
python scripts/export_manager.py dev-handoff "file-key" --include-specs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Limitations & Scope
|
||||||
|
|
||||||
|
### Read-Only Operations
|
||||||
|
This skill provides **read-only access** to Figma files through the REST API. It can:
|
||||||
|
- ✅ Extract data, components, and styles
|
||||||
|
- ✅ Export assets in multiple formats
|
||||||
|
- ✅ Analyze and audit design files
|
||||||
|
- ✅ Generate comprehensive reports
|
||||||
|
|
||||||
|
### What It Cannot Do
|
||||||
|
- ❌ **Modify existing files** (colors, text, components)
|
||||||
|
- ❌ **Create new designs** or components
|
||||||
|
- ❌ **Batch update** multiple files
|
||||||
|
- ❌ **Real-time collaboration** features
|
||||||
|
|
||||||
|
For file modifications, you would need to develop a **Figma plugin** using the Plugin API.
|
||||||
|
|
||||||
|
## Technical Features
|
||||||
|
|
||||||
|
### API Rate Limiting
|
||||||
|
Built-in rate limiting and retry logic to handle Figma's API constraints gracefully.
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
Comprehensive error handling with detailed logging and recovery suggestions.
|
||||||
|
|
||||||
|
### Multi-Format Support
|
||||||
|
Export assets in PNG, SVG, PDF, and WEBP with platform-specific sizing.
|
||||||
6
_meta.json
Normal file
6
_meta.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"ownerId": "kn74kjx2pnh0qkmk5mrsxxrtjh7zw4g8",
|
||||||
|
"slug": "figma",
|
||||||
|
"version": "2.1.0",
|
||||||
|
"publishedAt": 1769380900543
|
||||||
|
}
|
||||||
76
assets/templates/brand-guidelines-template.json
Normal file
76
assets/templates/brand-guidelines-template.json
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
{
|
||||||
|
"brand_name": "Your Brand",
|
||||||
|
"colors": {
|
||||||
|
"primary": "#007AFF",
|
||||||
|
"secondary": "#5856D6",
|
||||||
|
"success": "#34C759",
|
||||||
|
"warning": "#FF9500",
|
||||||
|
"error": "#FF3B30",
|
||||||
|
"neutral_100": "#FFFFFF",
|
||||||
|
"neutral_200": "#F2F2F7",
|
||||||
|
"neutral_300": "#C7C7CC",
|
||||||
|
"neutral_800": "#1C1C1E",
|
||||||
|
"neutral_900": "#000000"
|
||||||
|
},
|
||||||
|
"typography": {
|
||||||
|
"heading_primary": {
|
||||||
|
"font_family": "SF Pro Display",
|
||||||
|
"font_weight": "700",
|
||||||
|
"font_size": "32px",
|
||||||
|
"line_height": "1.2"
|
||||||
|
},
|
||||||
|
"heading_secondary": {
|
||||||
|
"font_family": "SF Pro Display",
|
||||||
|
"font_weight": "600",
|
||||||
|
"font_size": "24px",
|
||||||
|
"line_height": "1.25"
|
||||||
|
},
|
||||||
|
"body_large": {
|
||||||
|
"font_family": "SF Pro Text",
|
||||||
|
"font_weight": "400",
|
||||||
|
"font_size": "17px",
|
||||||
|
"line_height": "1.4"
|
||||||
|
},
|
||||||
|
"body_regular": {
|
||||||
|
"font_family": "SF Pro Text",
|
||||||
|
"font_weight": "400",
|
||||||
|
"font_size": "15px",
|
||||||
|
"line_height": "1.4"
|
||||||
|
},
|
||||||
|
"caption": {
|
||||||
|
"font_family": "SF Pro Text",
|
||||||
|
"font_weight": "400",
|
||||||
|
"font_size": "12px",
|
||||||
|
"line_height": "1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"spacing": {
|
||||||
|
"xs": "4px",
|
||||||
|
"sm": "8px",
|
||||||
|
"md": "16px",
|
||||||
|
"lg": "24px",
|
||||||
|
"xl": "32px",
|
||||||
|
"2xl": "48px",
|
||||||
|
"3xl": "64px"
|
||||||
|
},
|
||||||
|
"border_radius": {
|
||||||
|
"none": "0px",
|
||||||
|
"sm": "4px",
|
||||||
|
"md": "8px",
|
||||||
|
"lg": "12px",
|
||||||
|
"xl": "16px",
|
||||||
|
"full": "9999px"
|
||||||
|
},
|
||||||
|
"shadows": {
|
||||||
|
"sm": "0 1px 2px rgba(0, 0, 0, 0.05)",
|
||||||
|
"md": "0 4px 6px rgba(0, 0, 0, 0.1)",
|
||||||
|
"lg": "0 10px 15px rgba(0, 0, 0, 0.1)",
|
||||||
|
"xl": "0 20px 25px rgba(0, 0, 0, 0.1)"
|
||||||
|
},
|
||||||
|
"accessibility": {
|
||||||
|
"min_contrast_ratio": 4.5,
|
||||||
|
"min_touch_target": "44px",
|
||||||
|
"focus_indicator_color": "#007AFF",
|
||||||
|
"focus_indicator_width": "3px"
|
||||||
|
}
|
||||||
|
}
|
||||||
352
references/accessibility-guidelines.md
Normal file
352
references/accessibility-guidelines.md
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
# Accessibility Guidelines for Figma Design
|
||||||
|
|
||||||
|
Comprehensive WCAG compliance guide and accessibility best practices for inclusive design.
|
||||||
|
|
||||||
|
## WCAG 2.1 Compliance Levels
|
||||||
|
|
||||||
|
### Level A (Minimum)
|
||||||
|
Basic web accessibility features that should be present in all designs.
|
||||||
|
|
||||||
|
### Level AA (Standard)
|
||||||
|
Recommended level for most websites and applications. Removes major barriers to accessing content.
|
||||||
|
|
||||||
|
### Level AAA (Enhanced)
|
||||||
|
Highest level, required for specialized contexts but not recommended as general policy.
|
||||||
|
|
||||||
|
**Focus on AA compliance** - this covers the vast majority of accessibility needs without excessive constraints.
|
||||||
|
|
||||||
|
## Color and Visual Accessibility
|
||||||
|
|
||||||
|
### Color Contrast Requirements
|
||||||
|
|
||||||
|
#### WCAG AA Standards
|
||||||
|
- **Normal text**: 4.5:1 contrast ratio minimum
|
||||||
|
- **Large text** (18pt+ or 14pt+ bold): 3:1 contrast ratio minimum
|
||||||
|
- **UI components**: 3:1 contrast ratio for borders, icons, form controls
|
||||||
|
- **Graphics**: 3:1 contrast ratio for meaningful graphics
|
||||||
|
|
||||||
|
#### WCAG AAA Standards (Enhanced)
|
||||||
|
- **Normal text**: 7:1 contrast ratio
|
||||||
|
- **Large text**: 4.5:1 contrast ratio
|
||||||
|
|
||||||
|
#### Testing Tools in Figma
|
||||||
|
- **Stark plugin**: Real-time contrast checking
|
||||||
|
- **Color Oracle**: Color blindness simulation
|
||||||
|
- **WebAIM contrast checker**: External validation
|
||||||
|
|
||||||
|
### Color Usage Guidelines
|
||||||
|
|
||||||
|
#### Don't Rely on Color Alone
|
||||||
|
```
|
||||||
|
❌ Bad: "Click the green button to continue"
|
||||||
|
✅ Good: "Click the 'Continue' button (green) to proceed"
|
||||||
|
```
|
||||||
|
|
||||||
|
- Use icons, text labels, or patterns alongside color
|
||||||
|
- Ensure information is conveyed through multiple visual cues
|
||||||
|
- Test designs in grayscale to verify information accessibility
|
||||||
|
|
||||||
|
#### Color Blindness Considerations
|
||||||
|
- **Red-green color blindness** affects ~8% of men, ~0.5% of women
|
||||||
|
- **Blue-yellow color blindness** is less common but still significant
|
||||||
|
- Use tools like Colorblinding or Stark to test your designs
|
||||||
|
- Consider using shapes, patterns, or positions as additional indicators
|
||||||
|
|
||||||
|
### Typography Accessibility
|
||||||
|
|
||||||
|
#### Font Size Guidelines
|
||||||
|
- **Minimum body text**: 16px (12pt) for web
|
||||||
|
- **Minimum mobile text**: 16px (prevents zoom on iOS)
|
||||||
|
- **Large text threshold**: 18pt (24px) regular, 14pt (18.7px) bold
|
||||||
|
- **Line height**: 1.5x font size minimum for body text
|
||||||
|
- **Paragraph spacing**: At least 1.5x line height
|
||||||
|
|
||||||
|
#### Font Choice
|
||||||
|
- **Sans-serif fonts** generally more readable on screens
|
||||||
|
- **Avoid decorative fonts** for body text
|
||||||
|
- **System fonts** ensure consistency and performance
|
||||||
|
- **Web-safe fonts** for broader compatibility
|
||||||
|
|
||||||
|
#### Text Layout
|
||||||
|
- **Line length**: 45-75 characters for optimal readability
|
||||||
|
- **Left alignment** for left-to-right languages
|
||||||
|
- **Adequate spacing** between letters, words, lines, paragraphs
|
||||||
|
- **Avoid justified text** which can create awkward spacing
|
||||||
|
|
||||||
|
## Interactive Element Accessibility
|
||||||
|
|
||||||
|
### Touch and Click Targets
|
||||||
|
|
||||||
|
#### Size Requirements
|
||||||
|
- **Minimum size**: 44x44px (iOS/Material Design standard)
|
||||||
|
- **Recommended size**: 48x48px for better usability
|
||||||
|
- **Spacing**: At least 8px between adjacent targets
|
||||||
|
- **Mobile considerations**: Thumb-friendly zones, easy reach
|
||||||
|
|
||||||
|
#### Visual Feedback
|
||||||
|
- **Hover states**: Clear indication of interactive elements
|
||||||
|
- **Active states**: Immediate feedback on interaction
|
||||||
|
- **Disabled states**: Clearly distinguish non-functional elements
|
||||||
|
- **Loading states**: Show progress for time-consuming actions
|
||||||
|
|
||||||
|
### Focus Management
|
||||||
|
|
||||||
|
#### Focus Indicators
|
||||||
|
- **Visible focus**: Clear outline or background change
|
||||||
|
- **High contrast**: Focus indicator must have 3:1 contrast ratio
|
||||||
|
- **Consistent style**: Same focus treatment across the interface
|
||||||
|
- **Never remove focus indicators** without providing alternative
|
||||||
|
|
||||||
|
#### Focus Order
|
||||||
|
- **Logical sequence**: Follow visual layout and reading order
|
||||||
|
- **Tab navigation**: All interactive elements reachable via keyboard
|
||||||
|
- **Skip links**: Allow bypassing repetitive navigation
|
||||||
|
- **Focus traps**: Keep focus within modals/dialogs when open
|
||||||
|
|
||||||
|
### Form Accessibility
|
||||||
|
|
||||||
|
#### Label Requirements
|
||||||
|
- **All inputs must have labels**: Use explicit labels, not just placeholders
|
||||||
|
- **Required field indicators**: Clear marking of mandatory fields
|
||||||
|
- **Group related fields**: Use fieldsets and legends for grouped inputs
|
||||||
|
- **Help text**: Provide guidance when needed
|
||||||
|
|
||||||
|
#### Error Handling
|
||||||
|
```
|
||||||
|
❌ Bad: Red border with no explanation
|
||||||
|
✅ Good: "Email address is required" with clear visual indicator
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Specific error messages**: Explain what's wrong and how to fix it
|
||||||
|
- **Error summaries**: List all errors at top of form for screen readers
|
||||||
|
- **Inline validation**: Real-time feedback where helpful
|
||||||
|
- **Success confirmation**: Confirm successful form submissions
|
||||||
|
|
||||||
|
#### Form Layout
|
||||||
|
- **Single column layouts**: Easier to navigate and complete
|
||||||
|
- **Logical grouping**: Related fields grouped together
|
||||||
|
- **Progress indicators**: Show steps in multi-step forms
|
||||||
|
- **Clear submission**: Make it obvious how to submit the form
|
||||||
|
|
||||||
|
## Content Structure and Navigation
|
||||||
|
|
||||||
|
### Heading Hierarchy
|
||||||
|
|
||||||
|
#### Proper Heading Structure
|
||||||
|
```html
|
||||||
|
H1 - Page title (one per page)
|
||||||
|
├── H2 - Main sections
|
||||||
|
│ ├── H3 - Subsections
|
||||||
|
│ │ └── H4 - Sub-subsections
|
||||||
|
│ └── H3 - Another subsection
|
||||||
|
└── H2 - Another main section
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Don't skip levels**: H1 → H2 → H3, never H1 → H3
|
||||||
|
- **Use headings for structure**: Not just for visual styling
|
||||||
|
- **One H1 per page**: Primary page title only
|
||||||
|
|
||||||
|
### Link Accessibility
|
||||||
|
|
||||||
|
#### Link Text Guidelines
|
||||||
|
```
|
||||||
|
❌ Bad: "Click here for more information"
|
||||||
|
✅ Good: "Read our complete accessibility guide"
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Descriptive link text**: Explains where the link leads
|
||||||
|
- **Context independence**: Should make sense when read alone
|
||||||
|
- **Unique link text**: Different destinations need different text
|
||||||
|
- **External link indicators**: Show when links lead off-site
|
||||||
|
|
||||||
|
### Navigation Patterns
|
||||||
|
|
||||||
|
#### Skip Links
|
||||||
|
- **Skip to main content**: Bypass repetitive navigation
|
||||||
|
- **Skip to search**: Quick access to search functionality
|
||||||
|
- **Keyboard users**: Essential for efficient navigation
|
||||||
|
- **Hidden until focused**: Don't clutter visual design
|
||||||
|
|
||||||
|
#### Breadcrumbs
|
||||||
|
- **Show location**: Help users understand where they are
|
||||||
|
- **Provide navigation**: Easy way to move up the hierarchy
|
||||||
|
- **Current page**: Don't make current page a link
|
||||||
|
- **Separator clarity**: Use > or / with proper ARIA labels
|
||||||
|
|
||||||
|
## Images and Media
|
||||||
|
|
||||||
|
### Image Accessibility
|
||||||
|
|
||||||
|
#### Alt Text Guidelines
|
||||||
|
- **Decorative images**: Use empty alt attribute (alt="")
|
||||||
|
- **Informative images**: Describe the information conveyed
|
||||||
|
- **Functional images**: Describe the action/function
|
||||||
|
- **Complex images**: Provide detailed description nearby
|
||||||
|
|
||||||
|
#### Alt Text Examples
|
||||||
|
```
|
||||||
|
❌ Bad: alt="image"
|
||||||
|
❌ Bad: alt="photo.jpg"
|
||||||
|
✅ Good: alt="Bar chart showing 40% increase in sales"
|
||||||
|
✅ Good: alt="Submit form" (for submit button image)
|
||||||
|
✅ Good: alt="" (for purely decorative images)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Video and Audio
|
||||||
|
|
||||||
|
#### Video Accessibility
|
||||||
|
- **Captions**: For all spoken content
|
||||||
|
- **Audio descriptions**: For visual content not described in audio
|
||||||
|
- **Transcript**: Full text version of audio content
|
||||||
|
- **Player controls**: Accessible play/pause/volume controls
|
||||||
|
|
||||||
|
#### Audio Accessibility
|
||||||
|
- **Transcripts**: For all audio content
|
||||||
|
- **Auto-play restrictions**: Avoid auto-playing audio
|
||||||
|
- **Volume controls**: User control over audio levels
|
||||||
|
- **Visual indicators**: Show when audio is playing
|
||||||
|
|
||||||
|
## Mobile Accessibility
|
||||||
|
|
||||||
|
### Touch Interface Guidelines
|
||||||
|
|
||||||
|
#### Gesture Support
|
||||||
|
- **Single-tap primary**: Main interaction method
|
||||||
|
- **Alternative access**: Provide alternatives to complex gestures
|
||||||
|
- **Gesture hints**: Teach users about available gestures
|
||||||
|
- **Gesture conflicts**: Avoid conflicts with system gestures
|
||||||
|
|
||||||
|
#### Mobile-Specific Considerations
|
||||||
|
- **Orientation support**: Work in both portrait and landscape
|
||||||
|
- **Zoom support**: Allow pinch-to-zoom for text content
|
||||||
|
- **Motion sensitivity**: Respect reduced motion preferences
|
||||||
|
- **One-handed use**: Design for thumb navigation
|
||||||
|
|
||||||
|
### Screen Reader Support
|
||||||
|
|
||||||
|
#### iOS VoiceOver
|
||||||
|
- **Element labeling**: Provide clear, descriptive labels
|
||||||
|
- **Navigation order**: Logical focus sequence
|
||||||
|
- **Custom actions**: Define available actions for elements
|
||||||
|
- **Notifications**: Use announcements for dynamic changes
|
||||||
|
|
||||||
|
#### Android TalkBack
|
||||||
|
- **Content descriptions**: Equivalent to alt text for UI elements
|
||||||
|
- **Clickable indicators**: Mark interactive elements properly
|
||||||
|
- **Live regions**: Announce dynamic content changes
|
||||||
|
- **Semantic markup**: Use proper HTML/accessibility semantics
|
||||||
|
|
||||||
|
## Testing and Validation
|
||||||
|
|
||||||
|
### Automated Testing Tools
|
||||||
|
|
||||||
|
#### Figma Plugins
|
||||||
|
- **Stark**: Comprehensive accessibility checker
|
||||||
|
- **Color Blind Web Page Filter**: Color blindness simulation
|
||||||
|
- **Able**: Color contrast and font size checker
|
||||||
|
- **A11y - Color Contrast Checker**: Quick contrast validation
|
||||||
|
|
||||||
|
#### External Tools
|
||||||
|
- **WebAIM WAVE**: Web accessibility evaluation
|
||||||
|
- **axe DevTools**: Automated accessibility testing
|
||||||
|
- **Lighthouse**: Google's accessibility auditing
|
||||||
|
- **Pa11y**: Command-line accessibility testing
|
||||||
|
|
||||||
|
### Manual Testing Methods
|
||||||
|
|
||||||
|
#### Keyboard Testing
|
||||||
|
1. **Tab navigation**: Can you reach all interactive elements?
|
||||||
|
2. **Enter/Space activation**: Do buttons and links work?
|
||||||
|
3. **Arrow key navigation**: Works in menus and lists?
|
||||||
|
4. **Escape key**: Closes modals and menus?
|
||||||
|
|
||||||
|
#### Screen Reader Testing
|
||||||
|
1. **VoiceOver** (Mac): System Preferences → Accessibility → VoiceOver
|
||||||
|
2. **NVDA** (Windows): Free screen reader for testing
|
||||||
|
3. **JAWS** (Windows): Professional screen reader
|
||||||
|
4. **TalkBack** (Android): Built-in Android screen reader
|
||||||
|
|
||||||
|
#### Visual Testing
|
||||||
|
1. **Zoom to 200%**: Content should remain usable
|
||||||
|
2. **Grayscale mode**: Information still accessible?
|
||||||
|
3. **High contrast mode**: Text and UI still visible?
|
||||||
|
4. **Color blindness simulation**: Information still clear?
|
||||||
|
|
||||||
|
### User Testing
|
||||||
|
|
||||||
|
#### Include Users with Disabilities
|
||||||
|
- **Recruit diverse participants**: Different disabilities and assistive technologies
|
||||||
|
- **Test with real users**: Automated tools can't catch everything
|
||||||
|
- **Observe natural usage**: Don't guide too much during testing
|
||||||
|
- **Iterate based on feedback**: Accessibility is an ongoing process
|
||||||
|
|
||||||
|
#### Testing Scenarios
|
||||||
|
- **First-time usage**: Can new users complete key tasks?
|
||||||
|
- **Error recovery**: What happens when things go wrong?
|
||||||
|
- **Complex workflows**: Multi-step processes accessible?
|
||||||
|
- **Different contexts**: Various devices, environments, capabilities
|
||||||
|
|
||||||
|
## Implementation Guidelines
|
||||||
|
|
||||||
|
### Designer Handoff
|
||||||
|
|
||||||
|
#### Accessibility Annotations
|
||||||
|
- **Alt text specifications**: Document all image alt text
|
||||||
|
- **Focus order notes**: Specify tab sequence where non-obvious
|
||||||
|
- **Heading levels**: Mark proper heading hierarchy
|
||||||
|
- **Color contrast values**: Include specific contrast ratios
|
||||||
|
- **Interactive states**: Document all hover/focus/active states
|
||||||
|
|
||||||
|
#### Component Documentation
|
||||||
|
- **Accessibility features**: Built-in accessibility considerations
|
||||||
|
- **Usage guidelines**: When and how to use accessibly
|
||||||
|
- **ARIA patterns**: Required ARIA attributes and roles
|
||||||
|
- **Keyboard interactions**: Expected keyboard behavior
|
||||||
|
|
||||||
|
### Design System Integration
|
||||||
|
|
||||||
|
#### Accessible Components
|
||||||
|
- **Design once, use everywhere**: Build accessibility into components
|
||||||
|
- **Default accessibility**: Make accessible the easy choice
|
||||||
|
- **Clear documentation**: Accessibility requirements in design system
|
||||||
|
- **Regular audits**: Review and update component accessibility
|
||||||
|
|
||||||
|
#### Style Guidelines
|
||||||
|
- **Color palettes**: Pre-tested for contrast ratios
|
||||||
|
- **Typography scales**: Meet minimum size requirements
|
||||||
|
- **Spacing systems**: Ensure adequate touch targets
|
||||||
|
- **Icon libraries**: Include alt text recommendations
|
||||||
|
|
||||||
|
## Legal and Compliance
|
||||||
|
|
||||||
|
### Relevant Laws and Standards
|
||||||
|
|
||||||
|
#### United States
|
||||||
|
- **ADA** (Americans with Disabilities Act): Civil rights law
|
||||||
|
- **Section 508**: Federal agency accessibility requirements
|
||||||
|
- **WCAG 2.1**: Technical standard referenced by many laws
|
||||||
|
|
||||||
|
#### International
|
||||||
|
- **EN 301 549** (European Union): European accessibility standard
|
||||||
|
- **AODA** (Ontario): Accessibility for Ontarians with Disabilities Act
|
||||||
|
- **DDA** (Australia): Disability Discrimination Act
|
||||||
|
|
||||||
|
### Risk Mitigation
|
||||||
|
- **Legal compliance**: Following WCAG AA reduces legal risk
|
||||||
|
- **Documentation**: Keep records of accessibility efforts
|
||||||
|
- **Regular audits**: Ongoing compliance checking
|
||||||
|
- **User feedback**: Channels for reporting accessibility issues
|
||||||
|
|
||||||
|
## Resources and Tools
|
||||||
|
|
||||||
|
### Essential Resources
|
||||||
|
- **WCAG 2.1 Guidelines**: Official W3C accessibility standard
|
||||||
|
- **WebAIM**: Practical accessibility guidance and tools
|
||||||
|
- **A11y Project**: Community-driven accessibility resources
|
||||||
|
- **Inclusive Design Principles**: Microsoft's inclusive design guide
|
||||||
|
|
||||||
|
### Figma-Specific Resources
|
||||||
|
- **Figma Accessibility Guide**: Official Figma accessibility documentation
|
||||||
|
- **Accessible Design Systems**: Examples of accessible component libraries
|
||||||
|
- **Plugin Directory**: Accessibility-focused Figma plugins
|
||||||
|
- **Community Resources**: Accessibility templates and examples
|
||||||
294
references/design-patterns.md
Normal file
294
references/design-patterns.md
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
# Design Patterns & Component Best Practices
|
||||||
|
|
||||||
|
Comprehensive guide to UI patterns, component design, and design system best practices for Figma.
|
||||||
|
|
||||||
|
## Component Architecture
|
||||||
|
|
||||||
|
### Atomic Design Principles
|
||||||
|
|
||||||
|
#### Atoms (Basic Elements)
|
||||||
|
- **Buttons**: Primary, secondary, ghost, icon buttons
|
||||||
|
- **Form inputs**: Text fields, selectors, checkboxes, radio buttons
|
||||||
|
- **Typography**: Headings, body text, captions, labels
|
||||||
|
- **Icons**: Consistent icon library with standardized sizing
|
||||||
|
- **Avatars**: User profile images with fallback states
|
||||||
|
|
||||||
|
**Best Practices:**
|
||||||
|
- Use auto-layout for flexible resizing
|
||||||
|
- Create consistent hover/focus/disabled states
|
||||||
|
- Establish clear naming conventions
|
||||||
|
- Include component documentation
|
||||||
|
|
||||||
|
#### Molecules (Simple Combinations)
|
||||||
|
- **Form groups**: Label + input + validation message
|
||||||
|
- **Navigation items**: Icon + text + badge
|
||||||
|
- **Card headers**: Title + subtitle + actions
|
||||||
|
- **Search bars**: Input + search icon + clear button
|
||||||
|
|
||||||
|
**Best Practices:**
|
||||||
|
- Combine atoms logically and purposefully
|
||||||
|
- Maintain single responsibility principle
|
||||||
|
- Use component properties for variations
|
||||||
|
- Test across different content lengths
|
||||||
|
|
||||||
|
#### Organisms (Complex Combinations)
|
||||||
|
- **Navigation bars**: Logo + menu + user profile + search
|
||||||
|
- **Data tables**: Headers + rows + pagination + actions
|
||||||
|
- **Product cards**: Image + title + price + actions
|
||||||
|
- **Forms**: Multiple form groups + buttons + validation
|
||||||
|
|
||||||
|
**Best Practices:**
|
||||||
|
- Design for responsive behavior
|
||||||
|
- Consider loading and error states
|
||||||
|
- Plan for empty states and edge cases
|
||||||
|
- Optimize for accessibility
|
||||||
|
|
||||||
|
### Component Naming Conventions
|
||||||
|
|
||||||
|
#### Hierarchical Structure
|
||||||
|
```
|
||||||
|
Component Name / Variant / State
|
||||||
|
Examples:
|
||||||
|
- Button / Primary / Default
|
||||||
|
- Button / Primary / Hover
|
||||||
|
- Button / Secondary / Disabled
|
||||||
|
- Input / Text / Error
|
||||||
|
- Card / Product / Loading
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Descriptive Naming
|
||||||
|
- Use descriptive, action-oriented names
|
||||||
|
- Avoid technical jargon in user-facing names
|
||||||
|
- Be consistent across similar components
|
||||||
|
- Include size/type indicators when helpful
|
||||||
|
|
||||||
|
## Layout Patterns
|
||||||
|
|
||||||
|
### Grid Systems
|
||||||
|
|
||||||
|
#### Standard Grid Configurations
|
||||||
|
- **12-column grid**: Most versatile, works for web and mobile
|
||||||
|
- **8-column grid**: Good for tablet layouts
|
||||||
|
- **4-column grid**: Mobile-friendly, simple layouts
|
||||||
|
- **Custom grids**: Match specific brand requirements
|
||||||
|
|
||||||
|
**Grid Properties:**
|
||||||
|
- Consistent gutters (16px, 20px, 24px common)
|
||||||
|
- Responsive breakpoints (320px, 768px, 1024px, 1440px)
|
||||||
|
- Maximum content width (1200px-1440px typical)
|
||||||
|
|
||||||
|
#### Auto-Layout Best Practices
|
||||||
|
- Use auto-layout for all flexible components
|
||||||
|
- Set appropriate resizing constraints
|
||||||
|
- Consider padding vs margin usage
|
||||||
|
- Test with varying content lengths
|
||||||
|
|
||||||
|
### Common Layout Patterns
|
||||||
|
|
||||||
|
#### Header Patterns
|
||||||
|
1. **Simple header**: Logo + navigation + CTA
|
||||||
|
2. **Mega menu**: Logo + dropdown navigation + search + account
|
||||||
|
3. **Mobile header**: Hamburger + logo + account/cart
|
||||||
|
4. **Dashboard header**: Breadcrumbs + title + actions
|
||||||
|
|
||||||
|
#### Content Layouts
|
||||||
|
1. **Single column**: Simple, focused content flow
|
||||||
|
2. **Two column**: Main content + sidebar
|
||||||
|
3. **Three column**: Sidebar + main + secondary sidebar
|
||||||
|
4. **Card grid**: Responsive card layouts
|
||||||
|
5. **Masonry**: Pinterest-style irregular grid
|
||||||
|
|
||||||
|
#### Footer Patterns
|
||||||
|
1. **Simple footer**: Copyright + key links
|
||||||
|
2. **Rich footer**: Multiple link columns + social + newsletter
|
||||||
|
3. **Sticky footer**: Always at bottom of viewport
|
||||||
|
4. **Fat footer**: Extensive links + contact info + sitemap
|
||||||
|
|
||||||
|
## Interface Patterns
|
||||||
|
|
||||||
|
### Navigation Patterns
|
||||||
|
|
||||||
|
#### Primary Navigation
|
||||||
|
- **Horizontal nav**: Works well for 3-7 main sections
|
||||||
|
- **Vertical sidebar**: Good for 8+ items or complex hierarchies
|
||||||
|
- **Tab navigation**: For equal-importance sections
|
||||||
|
- **Breadcrumbs**: Show hierarchy and allow backtracking
|
||||||
|
|
||||||
|
#### Secondary Navigation
|
||||||
|
- **Dropdown menus**: Organize related sub-items
|
||||||
|
- **Contextual sidebars**: Show relevant options for current content
|
||||||
|
- **Floating action buttons**: Promote primary actions
|
||||||
|
- **Bottom navigation**: Mobile-friendly for core functions
|
||||||
|
|
||||||
|
### Form Patterns
|
||||||
|
|
||||||
|
#### Form Layout
|
||||||
|
- **Single column**: Easier to scan and complete
|
||||||
|
- **Label placement**: Above fields for better readability
|
||||||
|
- **Required indicators**: Use asterisks or "(required)" text
|
||||||
|
- **Help text**: Provide when needed, but don't overdo
|
||||||
|
|
||||||
|
#### Input Patterns
|
||||||
|
- **Progressive disclosure**: Show additional fields as needed
|
||||||
|
- **Smart defaults**: Pre-fill when possible
|
||||||
|
- **Inline validation**: Real-time feedback on field completion
|
||||||
|
- **Clear error states**: Specific, actionable error messages
|
||||||
|
|
||||||
|
#### Form Actions
|
||||||
|
- **Primary/secondary buttons**: Clear visual hierarchy
|
||||||
|
- **Save states**: Show progress and confirmation
|
||||||
|
- **Cancel behavior**: Ask about unsaved changes
|
||||||
|
- **Multi-step forms**: Show progress and allow navigation
|
||||||
|
|
||||||
|
### Data Display Patterns
|
||||||
|
|
||||||
|
#### Tables
|
||||||
|
- **Sortable headers**: Allow data organization
|
||||||
|
- **Pagination**: Handle large datasets
|
||||||
|
- **Row actions**: Edit, delete, view details
|
||||||
|
- **Selection**: Bulk operations capability
|
||||||
|
- **Responsive behavior**: Stack or hide columns on mobile
|
||||||
|
|
||||||
|
#### Cards
|
||||||
|
- **Consistent structure**: Image + title + metadata + actions
|
||||||
|
- **Hover states**: Show additional information or actions
|
||||||
|
- **Loading states**: Skeleton screens or progress indicators
|
||||||
|
- **Empty states**: Helpful guidance when no content exists
|
||||||
|
|
||||||
|
#### Lists
|
||||||
|
- **Simple lists**: Basic text with optional icons
|
||||||
|
- **Rich lists**: Multiple lines of information
|
||||||
|
- **Interactive lists**: Drag-and-drop, selection
|
||||||
|
- **Infinite scroll**: Load more content seamlessly
|
||||||
|
|
||||||
|
## Responsive Design Patterns
|
||||||
|
|
||||||
|
### Breakpoint Strategy
|
||||||
|
|
||||||
|
#### Common Breakpoints
|
||||||
|
- **Mobile**: 320px - 767px
|
||||||
|
- **Tablet**: 768px - 1023px
|
||||||
|
- **Desktop**: 1024px - 1439px
|
||||||
|
- **Large desktop**: 1440px+
|
||||||
|
|
||||||
|
#### Content Strategy
|
||||||
|
- **Mobile first**: Design for constraints, enhance for larger screens
|
||||||
|
- **Progressive enhancement**: Add features as screen size allows
|
||||||
|
- **Content parity**: Ensure feature availability across devices
|
||||||
|
- **Touch targets**: Minimum 44px for mobile interactions
|
||||||
|
|
||||||
|
### Adaptive Techniques
|
||||||
|
|
||||||
|
#### Navigation Adaptation
|
||||||
|
- **Collapsible menu**: Hamburger pattern for mobile
|
||||||
|
- **Priority navigation**: Show most important items first
|
||||||
|
- **Overflow menus**: "More" option for secondary items
|
||||||
|
- **Tab bar**: Bottom navigation for mobile apps
|
||||||
|
|
||||||
|
#### Content Adaptation
|
||||||
|
- **Stacking**: Single column on mobile, multiple on desktop
|
||||||
|
- **Content reduction**: Progressive disclosure on smaller screens
|
||||||
|
- **Image scaling**: Responsive images with appropriate crops
|
||||||
|
- **Typography scaling**: Larger text on mobile for readability
|
||||||
|
|
||||||
|
## Accessibility Patterns
|
||||||
|
|
||||||
|
### Color and Contrast
|
||||||
|
- **4.5:1 contrast ratio**: Minimum for normal text (WCAG AA)
|
||||||
|
- **3:1 contrast ratio**: Minimum for large text and UI components
|
||||||
|
- **Don't rely on color alone**: Use icons, text, or patterns too
|
||||||
|
- **Color blind considerations**: Test with color vision simulators
|
||||||
|
|
||||||
|
### Interaction Patterns
|
||||||
|
- **Focus indicators**: Clear visual focus for keyboard navigation
|
||||||
|
- **Touch targets**: Minimum 44x44px for touch interfaces
|
||||||
|
- **Click/tap areas**: Generous padding around interactive elements
|
||||||
|
- **Hover states**: Clear feedback for interactive elements
|
||||||
|
|
||||||
|
### Content Patterns
|
||||||
|
- **Alt text**: Descriptive text for images and icons
|
||||||
|
- **Heading hierarchy**: Proper H1-H6 structure
|
||||||
|
- **Link text**: Descriptive, avoid "click here"
|
||||||
|
- **Form labels**: Clear, descriptive labels for all inputs
|
||||||
|
|
||||||
|
## Animation and Microinteractions
|
||||||
|
|
||||||
|
### Animation Principles
|
||||||
|
- **Purposeful motion**: Animation should serve a function
|
||||||
|
- **Consistent timing**: Use consistent easing and duration
|
||||||
|
- **Respect preferences**: Honor reduced motion preferences
|
||||||
|
- **Performance**: Smooth 60fps animations
|
||||||
|
|
||||||
|
### Common Microinteractions
|
||||||
|
- **Button feedback**: Subtle scale or color change on press
|
||||||
|
- **Loading indicators**: Skeleton screens or spinners
|
||||||
|
- **Success confirmations**: Checkmarks or brief messaging
|
||||||
|
- **Error handling**: Gentle shake or color change for errors
|
||||||
|
- **Page transitions**: Smooth movement between states
|
||||||
|
|
||||||
|
### Transition Patterns
|
||||||
|
- **Slide transitions**: Natural for sequential content
|
||||||
|
- **Fade transitions**: Good for overlays and modals
|
||||||
|
- **Scale transitions**: Effective for showing/hiding elements
|
||||||
|
- **Morphing transitions**: Transform one element into another
|
||||||
|
|
||||||
|
## Design System Organization
|
||||||
|
|
||||||
|
### File Structure
|
||||||
|
```
|
||||||
|
Design System/
|
||||||
|
├── Foundation/
|
||||||
|
│ ├── Colors
|
||||||
|
│ ├── Typography
|
||||||
|
│ ├── Spacing
|
||||||
|
│ ├── Grid
|
||||||
|
│ └── Iconography
|
||||||
|
├── Components/
|
||||||
|
│ ├── Atoms/
|
||||||
|
│ ├── Molecules/
|
||||||
|
│ └── Organisms/
|
||||||
|
├── Patterns/
|
||||||
|
│ ├── Navigation
|
||||||
|
│ ├── Forms
|
||||||
|
│ ├── Data Display
|
||||||
|
│ └── Feedback
|
||||||
|
└── Templates/
|
||||||
|
├── Landing Pages
|
||||||
|
├── Dashboard
|
||||||
|
└── Content Pages
|
||||||
|
```
|
||||||
|
|
||||||
|
### Documentation Standards
|
||||||
|
- **Component purpose**: What problem does it solve?
|
||||||
|
- **Usage guidelines**: When and how to use
|
||||||
|
- **Do's and don'ts**: Clear examples of proper usage
|
||||||
|
- **Accessibility notes**: ARIA patterns, keyboard behavior
|
||||||
|
- **Implementation notes**: Technical considerations
|
||||||
|
|
||||||
|
### Maintenance Practices
|
||||||
|
- **Regular audits**: Review and update components quarterly
|
||||||
|
- **Usage tracking**: Monitor which components are actually used
|
||||||
|
- **Feedback loops**: Collect input from designers and developers
|
||||||
|
- **Version control**: Clear versioning and change logs
|
||||||
|
- **Testing**: Validate components across different contexts
|
||||||
|
|
||||||
|
## Mobile-Specific Patterns
|
||||||
|
|
||||||
|
### Touch Interactions
|
||||||
|
- **Tap**: Primary interaction method
|
||||||
|
- **Long press**: Secondary actions, context menus
|
||||||
|
- **Swipe**: Navigation, dismissal actions
|
||||||
|
- **Pinch**: Zoom functionality
|
||||||
|
- **Pull to refresh**: Common mobile pattern
|
||||||
|
|
||||||
|
### Mobile Navigation
|
||||||
|
- **Tab bar**: 3-5 primary sections
|
||||||
|
- **Hamburger menu**: Secondary navigation
|
||||||
|
- **Segmented control**: Filter or view switching
|
||||||
|
- **Bottom sheet**: Contextual actions and options
|
||||||
|
|
||||||
|
### Mobile Content
|
||||||
|
- **Card-based layouts**: Easy to scan and interact with
|
||||||
|
- **Thumb-friendly zones**: Important actions in easy reach
|
||||||
|
- **Generous whitespace**: Improve readability and touch accuracy
|
||||||
|
- **Clear hierarchy**: Bold typography and visual separation
|
||||||
412
references/export-formats.md
Normal file
412
references/export-formats.md
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
# Export Formats and Specifications
|
||||||
|
|
||||||
|
Complete guide to Figma export options, formats, and optimization strategies for different use cases.
|
||||||
|
|
||||||
|
## Supported Export Formats
|
||||||
|
|
||||||
|
### Raster Formats
|
||||||
|
|
||||||
|
#### PNG (Portable Network Graphics)
|
||||||
|
**Best for:** UI elements, icons with transparency, screenshots
|
||||||
|
|
||||||
|
**Characteristics:**
|
||||||
|
- Lossless compression
|
||||||
|
- Supports transparency
|
||||||
|
- Larger file sizes than JPG
|
||||||
|
- Perfect for designs with sharp edges
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
- App icons and UI elements
|
||||||
|
- Logos with transparency
|
||||||
|
- Screenshots and mockups
|
||||||
|
- Print materials requiring transparency
|
||||||
|
|
||||||
|
**Export settings:**
|
||||||
|
- Scale: 1x, 2x, 3x, 4x
|
||||||
|
- Recommended: 2x for web, 3x for mobile apps
|
||||||
|
- Transparent backgrounds supported
|
||||||
|
|
||||||
|
#### JPG (Joint Photographic Experts Group)
|
||||||
|
**Best for:** Photographs, complex images, web optimization
|
||||||
|
|
||||||
|
**Characteristics:**
|
||||||
|
- Lossy compression
|
||||||
|
- Smaller file sizes
|
||||||
|
- No transparency support
|
||||||
|
- Good for photographic content
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
- Hero images and photography
|
||||||
|
- Marketing materials
|
||||||
|
- Email templates
|
||||||
|
- Web banners where file size matters
|
||||||
|
|
||||||
|
**Export settings:**
|
||||||
|
- Quality levels available in some tools
|
||||||
|
- Automatic white background fill
|
||||||
|
- Scale options: 1x, 2x, 4x
|
||||||
|
|
||||||
|
#### WEBP
|
||||||
|
**Best for:** Web optimization, modern browsers
|
||||||
|
|
||||||
|
**Characteristics:**
|
||||||
|
- Superior compression to PNG/JPG
|
||||||
|
- Supports transparency and animation
|
||||||
|
- Smaller file sizes
|
||||||
|
- Not supported in all browsers
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
- Web assets for modern browsers
|
||||||
|
- Progressive web apps
|
||||||
|
- Performance-critical applications
|
||||||
|
|
||||||
|
### Vector Formats
|
||||||
|
|
||||||
|
#### SVG (Scalable Vector Graphics)
|
||||||
|
**Best for:** Icons, simple illustrations, scalable graphics
|
||||||
|
|
||||||
|
**Characteristics:**
|
||||||
|
- Infinitely scalable
|
||||||
|
- Small file sizes for simple graphics
|
||||||
|
- Editable code
|
||||||
|
- Supports interactive elements
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
- Icon libraries
|
||||||
|
- Simple illustrations
|
||||||
|
- Logos for web use
|
||||||
|
- Scalable graphics
|
||||||
|
|
||||||
|
**Export options:**
|
||||||
|
- `svg_include_id`: Include node IDs for manipulation
|
||||||
|
- `svg_simplify_stroke`: Optimize stroke paths
|
||||||
|
- Text handling: Convert to paths vs keep as text
|
||||||
|
|
||||||
|
#### PDF (Portable Document Format)
|
||||||
|
**Best for:** Print materials, presentations, documentation
|
||||||
|
|
||||||
|
**Characteristics:**
|
||||||
|
- Vector-based when possible
|
||||||
|
- High quality for print
|
||||||
|
- Preserves text and formatting
|
||||||
|
- Universal compatibility
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
- Print marketing materials
|
||||||
|
- Presentations
|
||||||
|
- Documentation handoff
|
||||||
|
- High-quality mockups
|
||||||
|
|
||||||
|
**Export settings:**
|
||||||
|
- Vector elements preserved when possible
|
||||||
|
- Raster elements included at appropriate resolution
|
||||||
|
- Text can remain selectable
|
||||||
|
|
||||||
|
## Export Scales and Resolutions
|
||||||
|
|
||||||
|
### Device Pixel Ratios
|
||||||
|
|
||||||
|
#### 1x (Standard Resolution)
|
||||||
|
- **Use for:** Web designs, standard monitors
|
||||||
|
- **Pixel density:** 96 DPI
|
||||||
|
- **File size:** Smallest
|
||||||
|
- **Quality:** Standard
|
||||||
|
|
||||||
|
#### 2x (High-DPI)
|
||||||
|
- **Use for:** Retina displays, high-DPI web
|
||||||
|
- **Pixel density:** 192 DPI
|
||||||
|
- **File size:** 4x larger than 1x
|
||||||
|
- **Quality:** Sharp on high-DPI screens
|
||||||
|
|
||||||
|
#### 3x (Mobile High-DPI)
|
||||||
|
- **Use for:** iPhone Plus, Android high-end devices
|
||||||
|
- **Pixel density:** 288 DPI
|
||||||
|
- **File size:** 9x larger than 1x
|
||||||
|
- **Quality:** Extremely sharp mobile displays
|
||||||
|
|
||||||
|
#### 4x (Maximum Resolution)
|
||||||
|
- **Use for:** Future-proofing, print materials
|
||||||
|
- **Pixel density:** 384 DPI
|
||||||
|
- **File size:** 16x larger than 1x
|
||||||
|
- **Quality:** Maximum detail
|
||||||
|
|
||||||
|
### Platform-Specific Recommendations
|
||||||
|
|
||||||
|
#### iOS Apps
|
||||||
|
- **1x:** iPhone 3GS and older (rarely needed)
|
||||||
|
- **2x:** iPhone 4-8, iPad non-Retina
|
||||||
|
- **3x:** iPhone 6 Plus and newer large iPhones
|
||||||
|
- **Required:** All three scales for App Store submission
|
||||||
|
|
||||||
|
#### Android Apps
|
||||||
|
- **ldpi (0.75x):** Low-density screens (rarely used)
|
||||||
|
- **mdpi (1x):** Medium-density baseline
|
||||||
|
- **hdpi (1.5x):** High-density screens
|
||||||
|
- **xhdpi (2x):** Extra high-density
|
||||||
|
- **xxhdpi (3x):** Extra extra high-density
|
||||||
|
- **xxxhdpi (4x):** Highest density screens
|
||||||
|
|
||||||
|
#### Web Development
|
||||||
|
- **1x:** Base resolution for all browsers
|
||||||
|
- **2x:** For `@2x` media queries and Retina displays
|
||||||
|
- **Consider WEBP:** For modern browsers with fallback
|
||||||
|
|
||||||
|
## Asset Organization Strategies
|
||||||
|
|
||||||
|
### Folder Structure
|
||||||
|
|
||||||
|
#### By Platform
|
||||||
|
```
|
||||||
|
assets/
|
||||||
|
├── web/
|
||||||
|
│ ├── 1x/
|
||||||
|
│ ├── 2x/
|
||||||
|
│ └── icons/
|
||||||
|
├── ios/
|
||||||
|
│ ├── 1x/
|
||||||
|
│ ├── 2x/
|
||||||
|
│ └── 3x/
|
||||||
|
└── android/
|
||||||
|
├── ldpi/
|
||||||
|
├── mdpi/
|
||||||
|
├── hdpi/
|
||||||
|
├── xhdpi/
|
||||||
|
├── xxhdpi/
|
||||||
|
└── xxxhdpi/
|
||||||
|
```
|
||||||
|
|
||||||
|
#### By Component Type
|
||||||
|
```
|
||||||
|
assets/
|
||||||
|
├── icons/
|
||||||
|
│ ├── navigation/
|
||||||
|
│ ├── actions/
|
||||||
|
│ └── status/
|
||||||
|
├── images/
|
||||||
|
│ ├── heroes/
|
||||||
|
│ ├── thumbnails/
|
||||||
|
│ └── placeholders/
|
||||||
|
└── logos/
|
||||||
|
├── full-color/
|
||||||
|
├── monochrome/
|
||||||
|
└── reversed/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Naming Conventions
|
||||||
|
|
||||||
|
#### Descriptive Naming
|
||||||
|
```
|
||||||
|
✅ Good:
|
||||||
|
- icon-search-24px.svg
|
||||||
|
- button-primary-large@2x.png
|
||||||
|
- hero-homepage-1200w.jpg
|
||||||
|
|
||||||
|
❌ Bad:
|
||||||
|
- icon1.svg
|
||||||
|
- button.png
|
||||||
|
- image.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Platform Conventions
|
||||||
|
|
||||||
|
**iOS:**
|
||||||
|
```
|
||||||
|
icon-name.png (1x)
|
||||||
|
icon-name@2x.png (2x)
|
||||||
|
icon-name@3x.png (3x)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Android:**
|
||||||
|
```
|
||||||
|
ic_name.png (mdpi)
|
||||||
|
ic_name_hdpi.png (hdpi)
|
||||||
|
ic_name_xhdpi.png (xhdpi)
|
||||||
|
ic_name_xxhdpi.png (xxhdpi)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Web:**
|
||||||
|
```
|
||||||
|
icon-name.svg (vector)
|
||||||
|
icon-name.png (1x fallback)
|
||||||
|
icon-name@2x.png (2x for Retina)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Optimization Techniques
|
||||||
|
|
||||||
|
### File Size Optimization
|
||||||
|
|
||||||
|
#### PNG Optimization
|
||||||
|
- **Reduce colors:** Use 8-bit when possible instead of 24-bit
|
||||||
|
- **Remove metadata:** Strip EXIF data and comments
|
||||||
|
- **Optimize palettes:** Use indexed color for simple graphics
|
||||||
|
- **Tools:** TinyPNG, ImageOptim, OptiPNG
|
||||||
|
|
||||||
|
#### JPG Optimization
|
||||||
|
- **Quality settings:** 80-90% for most use cases
|
||||||
|
- **Progressive JPEG:** Better perceived loading
|
||||||
|
- **Appropriate dimensions:** Don't export larger than needed
|
||||||
|
- **Tools:** JPEGmini, ImageOptim, MozJPEG
|
||||||
|
|
||||||
|
#### SVG Optimization
|
||||||
|
- **Simplify paths:** Remove unnecessary points
|
||||||
|
- **Group similar elements:** Reduce code duplication
|
||||||
|
- **Remove unused definitions:** Clean up gradients, styles
|
||||||
|
- **Tools:** SVGO, SVGOMG, Figma's built-in optimization
|
||||||
|
|
||||||
|
### Performance Considerations
|
||||||
|
|
||||||
|
#### Image Dimensions
|
||||||
|
- **Web images:** No larger than container size
|
||||||
|
- **2x images:** Exactly 2x the display size
|
||||||
|
- **Responsive images:** Multiple sizes for different breakpoints
|
||||||
|
- **Lazy loading:** Consider loading strategies
|
||||||
|
|
||||||
|
#### Format Selection Decision Tree
|
||||||
|
```
|
||||||
|
Is it a photograph or complex image?
|
||||||
|
├── Yes → JPG (or WEBP for modern browsers)
|
||||||
|
└── No → Does it need transparency?
|
||||||
|
├── Yes → PNG (or SVG if simple)
|
||||||
|
└── No → JPG for web, PNG for UI elements
|
||||||
|
```
|
||||||
|
|
||||||
|
## Design Handoff Specifications
|
||||||
|
|
||||||
|
### Developer Handoff Assets
|
||||||
|
|
||||||
|
#### Complete Asset Package
|
||||||
|
1. **All required scales:** Platform-specific requirements
|
||||||
|
2. **Multiple formats:** SVG + PNG fallbacks for icons
|
||||||
|
3. **Organized structure:** Clear folder organization
|
||||||
|
4. **Naming documentation:** Explain naming conventions
|
||||||
|
5. **Usage guidelines:** When to use each asset
|
||||||
|
|
||||||
|
#### Asset Specifications Document
|
||||||
|
```
|
||||||
|
Asset Name: primary-button-large
|
||||||
|
Formats Available: PNG (1x, 2x, 3x), SVG
|
||||||
|
Dimensions:
|
||||||
|
- 1x: 120x44px
|
||||||
|
- 2x: 240x88px
|
||||||
|
- 3x: 360x132px
|
||||||
|
Usage: Primary call-to-action buttons
|
||||||
|
States: Default, Hover, Active, Disabled
|
||||||
|
```
|
||||||
|
|
||||||
|
### Design System Documentation
|
||||||
|
|
||||||
|
#### Component Assets
|
||||||
|
- **Multiple states:** Default, hover, active, disabled, loading
|
||||||
|
- **Size variations:** Small, medium, large
|
||||||
|
- **Theme variations:** Light mode, dark mode
|
||||||
|
- **Context usage:** When and where to use each variation
|
||||||
|
|
||||||
|
#### Icon Libraries
|
||||||
|
- **Consistent sizing:** 16px, 24px, 32px standard sizes
|
||||||
|
- **Stroke weights:** Consistent line thickness across set
|
||||||
|
- **Style coherence:** Same visual style for entire set
|
||||||
|
- **Semantic grouping:** Organize by function or category
|
||||||
|
|
||||||
|
## Batch Export Strategies
|
||||||
|
|
||||||
|
### Figma Export Tips
|
||||||
|
|
||||||
|
#### Selection-Based Export
|
||||||
|
1. Select multiple frames/components
|
||||||
|
2. Use export panel for batch settings
|
||||||
|
3. Apply same settings to all selected items
|
||||||
|
4. Export to organized folder structure
|
||||||
|
|
||||||
|
#### Component-Based Workflow
|
||||||
|
1. Create export-ready components
|
||||||
|
2. Use consistent naming for automatic organization
|
||||||
|
3. Set up export settings as part of component definition
|
||||||
|
4. Use plugins for advanced batch operations
|
||||||
|
|
||||||
|
### Automation Opportunities
|
||||||
|
|
||||||
|
#### Script-Based Export
|
||||||
|
- **Figma API:** Programmatic export control
|
||||||
|
- **Custom tools:** Build specific export workflows
|
||||||
|
- **Batch processing:** Handle hundreds of assets efficiently
|
||||||
|
- **Quality assurance:** Automated optimization and validation
|
||||||
|
|
||||||
|
#### CI/CD Integration
|
||||||
|
- **Automated exports:** Trigger on design updates
|
||||||
|
- **Asset deployment:** Push directly to CDN or asset pipeline
|
||||||
|
- **Version control:** Track asset changes alongside code
|
||||||
|
- **Optimization pipeline:** Automatic image optimization
|
||||||
|
|
||||||
|
## Special Use Cases
|
||||||
|
|
||||||
|
### App Store Assets
|
||||||
|
|
||||||
|
#### iOS App Store
|
||||||
|
- **App icons:** 1024x1024px for store, various sizes for app
|
||||||
|
- **Screenshots:** Device-specific dimensions
|
||||||
|
- **Requirements:** No transparency, specific format requirements
|
||||||
|
- **Validation:** App Store Connect validation rules
|
||||||
|
|
||||||
|
#### Google Play Store
|
||||||
|
- **Feature graphic:** 1024x500px
|
||||||
|
- **Screenshots:** Various device categories
|
||||||
|
- **App icons:** 512x512px high-res icon
|
||||||
|
- **Requirements:** Specific aspect ratios and content guidelines
|
||||||
|
|
||||||
|
### Print Materials
|
||||||
|
|
||||||
|
#### Print Specifications
|
||||||
|
- **Resolution:** 300 DPI minimum for professional printing
|
||||||
|
- **Color mode:** CMYK for print, RGB for digital
|
||||||
|
- **Bleed areas:** Extra space beyond trim line
|
||||||
|
- **Safe areas:** Keep important content away from edges
|
||||||
|
|
||||||
|
#### Export Settings
|
||||||
|
- **PDF format:** Preferred for print handoff
|
||||||
|
- **High resolution:** Use 4x scale or higher
|
||||||
|
- **Color profiles:** Include ICC profiles when possible
|
||||||
|
- **Vector preservation:** Maintain vector elements where possible
|
||||||
|
|
||||||
|
### Email Templates
|
||||||
|
|
||||||
|
#### Email Constraints
|
||||||
|
- **Image blocking:** Many clients block images by default
|
||||||
|
- **File size limits:** Keep images under 100KB when possible
|
||||||
|
- **Fallback text:** ALT text for accessibility
|
||||||
|
- **Dimensions:** Consider mobile email clients
|
||||||
|
|
||||||
|
#### Optimization Strategy
|
||||||
|
- **JPG for photos:** Smaller file sizes
|
||||||
|
- **PNG for UI elements:** Crisp edges and transparency
|
||||||
|
- **Inline critical images:** Small logos and icons
|
||||||
|
- **CDN hosting:** Fast loading from reliable servers
|
||||||
|
|
||||||
|
## Quality Assurance
|
||||||
|
|
||||||
|
### Export Validation
|
||||||
|
|
||||||
|
#### Visual Inspection
|
||||||
|
- **Compare to original:** Side-by-side comparison
|
||||||
|
- **Different scales:** Verify all export scales look correct
|
||||||
|
- **Multiple devices:** Test on target devices/browsers
|
||||||
|
- **Print proofs:** Physical proofs for print materials
|
||||||
|
|
||||||
|
#### Technical Validation
|
||||||
|
- **File sizes:** Reasonable for intended use
|
||||||
|
- **Dimensions:** Correct pixel dimensions
|
||||||
|
- **Format compatibility:** Works in target environments
|
||||||
|
- **Color accuracy:** Colors match design intent
|
||||||
|
|
||||||
|
### Testing Workflows
|
||||||
|
|
||||||
|
#### Cross-Platform Testing
|
||||||
|
- **Multiple browsers:** Chrome, Firefox, Safari, Edge
|
||||||
|
- **Different devices:** iOS, Android, various screen sizes
|
||||||
|
- **Operating systems:** macOS, Windows, Linux
|
||||||
|
- **Assistive technology:** Screen readers, high contrast modes
|
||||||
|
|
||||||
|
#### Performance Testing
|
||||||
|
- **Load times:** Measure actual loading performance
|
||||||
|
- **Bandwidth testing:** Test on slow connections
|
||||||
|
- **Caching behavior:** Verify proper caching headers
|
||||||
|
- **CDN performance:** Test global delivery speeds
|
||||||
263
references/figma-api-reference.md
Normal file
263
references/figma-api-reference.md
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
# Figma API Reference
|
||||||
|
|
||||||
|
Complete reference for Figma REST API endpoints and Plugin API capabilities.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
### Access Token Setup
|
||||||
|
1. Generate personal access token: Figma → Settings → Account → Personal Access Tokens
|
||||||
|
2. For team/organization usage: Create OAuth app for broader access
|
||||||
|
3. Set environment variable: `FIGMA_ACCESS_TOKEN=your_token_here`
|
||||||
|
|
||||||
|
### API Headers
|
||||||
|
```http
|
||||||
|
X-Figma-Token: your_access_token_here
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
## REST API Endpoints
|
||||||
|
|
||||||
|
### File Operations
|
||||||
|
|
||||||
|
#### GET /v1/files/:key
|
||||||
|
Get complete file data including document tree, components, and styles.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `version` (optional): Specific version ID
|
||||||
|
- `ids` (optional): Comma-separated node IDs to limit scope
|
||||||
|
- `depth` (optional): How deep to traverse document tree
|
||||||
|
- `geometry` (optional): Set to "paths" for vector data
|
||||||
|
- `plugin_data` (optional): Plugin IDs to include plugin data
|
||||||
|
|
||||||
|
**Response includes:**
|
||||||
|
- Document tree with all nodes
|
||||||
|
- Components map with metadata
|
||||||
|
- Styles map with style definitions
|
||||||
|
- Version and file metadata
|
||||||
|
|
||||||
|
#### GET /v1/files/:key/nodes
|
||||||
|
Get specific nodes from a file.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `ids` (required): Comma-separated node IDs
|
||||||
|
- `version`, `depth`, `geometry`, `plugin_data` (same as above)
|
||||||
|
|
||||||
|
#### GET /v1/images/:key
|
||||||
|
Export nodes as images.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `ids` (required): Node IDs to export
|
||||||
|
- `scale` (optional): 1, 2, or 4 (default: 1)
|
||||||
|
- `format` (optional): jpg, png, svg, or pdf (default: png)
|
||||||
|
- `svg_include_id` (optional): Include node IDs in SVG
|
||||||
|
- `svg_simplify_stroke` (optional): Simplify strokes in SVG
|
||||||
|
- `use_absolute_bounds` (optional): Use absolute coordinates
|
||||||
|
- `version` (optional): Specific version to export
|
||||||
|
|
||||||
|
**Returns:** Map of node IDs to image URLs (URLs expire after 30 days)
|
||||||
|
|
||||||
|
#### GET /v1/files/:key/images
|
||||||
|
Get image fill metadata from a file.
|
||||||
|
|
||||||
|
### Component Operations
|
||||||
|
|
||||||
|
#### GET /v1/files/:key/components
|
||||||
|
Get all components in a file.
|
||||||
|
|
||||||
|
#### GET /v1/components/:key
|
||||||
|
Get component metadata by component key.
|
||||||
|
|
||||||
|
#### GET /v1/teams/:team_id/components
|
||||||
|
Get team component library.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `page_size` (optional): Results per page (max 1000)
|
||||||
|
- `after` (optional): Pagination cursor
|
||||||
|
|
||||||
|
### Style Operations
|
||||||
|
|
||||||
|
#### GET /v1/files/:key/styles
|
||||||
|
Get all styles in a file.
|
||||||
|
|
||||||
|
#### GET /v1/styles/:key
|
||||||
|
Get style metadata by style key.
|
||||||
|
|
||||||
|
#### GET /v1/teams/:team_id/styles
|
||||||
|
Get team style library.
|
||||||
|
|
||||||
|
### Project Operations
|
||||||
|
|
||||||
|
#### GET /v1/teams/:team_id/projects
|
||||||
|
Get projects for a team.
|
||||||
|
|
||||||
|
#### GET /v1/projects/:project_id/files
|
||||||
|
Get files in a project.
|
||||||
|
|
||||||
|
### User Operations
|
||||||
|
|
||||||
|
#### GET /v1/me
|
||||||
|
Get current user information.
|
||||||
|
|
||||||
|
## Rate Limits
|
||||||
|
|
||||||
|
- 1000 requests per minute per access token
|
||||||
|
- Image exports: 100 requests per minute
|
||||||
|
- Use exponential backoff for 429 responses
|
||||||
|
- Monitor `X-RateLimit-*` headers
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Common HTTP Status Codes
|
||||||
|
- `400 Bad Request`: Invalid parameters
|
||||||
|
- `401 Unauthorized`: Invalid or missing access token
|
||||||
|
- `403 Forbidden`: Insufficient permissions
|
||||||
|
- `404 Not Found`: File or resource doesn't exist
|
||||||
|
- `429 Too Many Requests`: Rate limit exceeded
|
||||||
|
- `500 Internal Server Error`: Figma server error
|
||||||
|
|
||||||
|
### Error Response Format
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": 400,
|
||||||
|
"err": "Bad request: Invalid file key"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Node Types
|
||||||
|
|
||||||
|
### Document Structure
|
||||||
|
- `DOCUMENT` - Root document node
|
||||||
|
- `CANVAS` - Page/canvas node
|
||||||
|
- `FRAME` - Frame container
|
||||||
|
- `GROUP` - Group container
|
||||||
|
- `SECTION` - Section container
|
||||||
|
|
||||||
|
### Shape Nodes
|
||||||
|
- `RECTANGLE` - Rectangle shape
|
||||||
|
- `LINE` - Line shape
|
||||||
|
- `ELLIPSE` - Ellipse shape
|
||||||
|
- `POLYGON` - Polygon shape
|
||||||
|
- `STAR` - Star shape
|
||||||
|
- `VECTOR` - Vector shape
|
||||||
|
|
||||||
|
### Text and Components
|
||||||
|
- `TEXT` - Text node
|
||||||
|
- `COMPONENT` - Master component
|
||||||
|
- `COMPONENT_SET` - Component set (variants)
|
||||||
|
- `INSTANCE` - Component instance
|
||||||
|
|
||||||
|
### Special Nodes
|
||||||
|
- `BOOLEAN_OPERATION` - Boolean operation result
|
||||||
|
- `SLICE` - Export slice
|
||||||
|
- `STICKY` - Sticky note (FigJam)
|
||||||
|
- `CONNECTOR` - Connector line (FigJam)
|
||||||
|
|
||||||
|
## Plugin API Overview
|
||||||
|
|
||||||
|
The Plugin API allows creating, modifying, and analyzing design files through plugins.
|
||||||
|
|
||||||
|
### Key Capabilities
|
||||||
|
- **Create nodes**: Generate frames, shapes, text, components
|
||||||
|
- **Modify properties**: Update fills, strokes, effects, layout
|
||||||
|
- **Component management**: Create/update components and instances
|
||||||
|
- **Style operations**: Create and apply text/fill/effect styles
|
||||||
|
- **File operations**: Navigate pages, selection, document structure
|
||||||
|
|
||||||
|
### Plugin API Limitations
|
||||||
|
- Runs in browser sandbox environment
|
||||||
|
- Cannot directly access external APIs (use UI for HTTP requests)
|
||||||
|
- Limited file system access
|
||||||
|
- Must be installed/authorized by users
|
||||||
|
|
||||||
|
### Common Plugin Patterns
|
||||||
|
|
||||||
|
#### Creating Basic Shapes
|
||||||
|
```javascript
|
||||||
|
// Create rectangle
|
||||||
|
const rect = figma.createRectangle();
|
||||||
|
rect.resize(100, 100);
|
||||||
|
rect.fills = [{type: 'SOLID', color: {r: 1, g: 0, b: 0}}];
|
||||||
|
|
||||||
|
// Create text
|
||||||
|
const text = figma.createText();
|
||||||
|
await figma.loadFontAsync(text.fontName);
|
||||||
|
text.characters = "Hello World";
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Working with Components
|
||||||
|
```javascript
|
||||||
|
// Create component
|
||||||
|
const component = figma.createComponent();
|
||||||
|
component.name = "Button";
|
||||||
|
|
||||||
|
// Create instance
|
||||||
|
const instance = component.createInstance();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Traversing Document Tree
|
||||||
|
```javascript
|
||||||
|
function traverse(node) {
|
||||||
|
console.log(node.name, node.type);
|
||||||
|
if ("children" in node) {
|
||||||
|
for (const child of node.children) {
|
||||||
|
traverse(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
traverse(figma.root);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### API Usage
|
||||||
|
1. **Batch operations**: Group multiple API calls when possible
|
||||||
|
2. **Cache results**: Store file data to minimize repeat requests
|
||||||
|
3. **Use specific node IDs**: Limit data transfer with `ids` parameter
|
||||||
|
4. **Handle rate limits**: Implement exponential backoff
|
||||||
|
5. **Version awareness**: Use version parameter for consistency
|
||||||
|
|
||||||
|
### Image Exports
|
||||||
|
1. **Choose appropriate format**: PNG for complex images, SVG for icons
|
||||||
|
2. **Optimize scale**: Use scale=1 unless high-DPI needed
|
||||||
|
3. **Batch exports**: Export multiple nodes in single request
|
||||||
|
4. **Cache URLs**: Store image URLs but remember 30-day expiration
|
||||||
|
|
||||||
|
### Plugin Development
|
||||||
|
1. **Minimize processing**: Keep operations fast to avoid timeouts
|
||||||
|
2. **Progress feedback**: Show progress for long operations
|
||||||
|
3. **Error handling**: Gracefully handle missing fonts, permissions
|
||||||
|
4. **Memory management**: Clean up large data structures
|
||||||
|
5. **User consent**: Request permissions appropriately
|
||||||
|
|
||||||
|
### Security
|
||||||
|
1. **Token protection**: Never expose access tokens in client-side code
|
||||||
|
2. **Scope principle**: Use minimal required permissions
|
||||||
|
3. **Input validation**: Validate all user inputs and API responses
|
||||||
|
4. **Audit logs**: Track API usage for compliance
|
||||||
|
|
||||||
|
## Common Use Cases
|
||||||
|
|
||||||
|
### Design System Automation
|
||||||
|
- Extract design tokens (colors, typography, spacing)
|
||||||
|
- Generate code from components
|
||||||
|
- Sync design systems across files
|
||||||
|
- Audit design consistency
|
||||||
|
|
||||||
|
### Asset Generation
|
||||||
|
- Export marketing assets in multiple formats
|
||||||
|
- Generate app icons and favicons
|
||||||
|
- Create social media templates
|
||||||
|
- Produce print-ready assets
|
||||||
|
|
||||||
|
### Workflow Integration
|
||||||
|
- Connect designs to development tools
|
||||||
|
- Automate handoff documentation
|
||||||
|
- Version control for design files
|
||||||
|
- Collaborative review processes
|
||||||
|
|
||||||
|
### Quality Assurance
|
||||||
|
- Accessibility compliance checking
|
||||||
|
- Brand guideline validation
|
||||||
|
- Consistency auditing across projects
|
||||||
|
- Performance optimization recommendations
|
||||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
requests>=2.31.0
|
||||||
|
aiohttp>=3.9.0
|
||||||
|
pathlib
|
||||||
565
scripts/accessibility_checker.py
Normal file
565
scripts/accessibility_checker.py
Normal file
@@ -0,0 +1,565 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Figma Accessibility Checker - WCAG compliance validation
|
||||||
|
Specialized accessibility audit with detailed WCAG compliance checking.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import time
|
||||||
|
from typing import Dict, List, Optional, Union, Any, Tuple
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
try:
|
||||||
|
from figma_client import FigmaClient
|
||||||
|
except ImportError:
|
||||||
|
# Handle case where script is run directly
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
sys.path.append(str(Path(__file__).parent))
|
||||||
|
from figma_client import FigmaClient
|
||||||
|
|
||||||
|
class AccessibilityChecker:
|
||||||
|
"""WCAG-focused accessibility checker for Figma designs"""
|
||||||
|
|
||||||
|
def __init__(self, figma_client: FigmaClient):
|
||||||
|
self.client = figma_client
|
||||||
|
|
||||||
|
def check_wcag_compliance(self, file_key: str, level: str = 'AA') -> Dict[str, Any]:
|
||||||
|
"""Comprehensive WCAG compliance check"""
|
||||||
|
|
||||||
|
print(f"Checking WCAG {level} compliance for file: {file_key}")
|
||||||
|
|
||||||
|
file_data = self.client.get_file(file_key)
|
||||||
|
|
||||||
|
results = {
|
||||||
|
'file_key': file_key,
|
||||||
|
'file_name': file_data.get('name', 'Unknown'),
|
||||||
|
'wcag_level': level,
|
||||||
|
'timestamp': time.time(),
|
||||||
|
'compliance_score': 0,
|
||||||
|
'issues': [],
|
||||||
|
'summary': {}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run all WCAG checks
|
||||||
|
self._check_color_contrast(file_data, results, level)
|
||||||
|
self._check_touch_targets(file_data, results)
|
||||||
|
self._check_text_sizing(file_data, results)
|
||||||
|
self._check_focus_indicators(file_data, results)
|
||||||
|
|
||||||
|
# Calculate compliance score
|
||||||
|
results['compliance_score'] = self._calculate_compliance_score(results)
|
||||||
|
results['summary'] = self._generate_summary(results)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def _check_color_contrast(self, file_data: Dict[str, Any], results: Dict[str, Any], level: str):
|
||||||
|
"""Check color contrast ratios against WCAG standards"""
|
||||||
|
|
||||||
|
contrast_requirements = {
|
||||||
|
'AA': {'normal_text': 4.5, 'large_text': 3.0, 'ui_components': 3.0},
|
||||||
|
'AAA': {'normal_text': 7.0, 'large_text': 4.5, 'ui_components': 4.5}
|
||||||
|
}
|
||||||
|
|
||||||
|
requirements = contrast_requirements[level]
|
||||||
|
|
||||||
|
def check_node_contrast(node):
|
||||||
|
if node.get('type') == 'TEXT':
|
||||||
|
# Get text color
|
||||||
|
fills = node.get('fills', [])
|
||||||
|
if not fills:
|
||||||
|
return
|
||||||
|
|
||||||
|
text_color = fills[0].get('color', {})
|
||||||
|
if not text_color:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Estimate background color (simplified - would need parent analysis)
|
||||||
|
bg_color = {'r': 1, 'g': 1, 'b': 1} # Assume white background
|
||||||
|
|
||||||
|
contrast_ratio = self._calculate_contrast_ratio(text_color, bg_color)
|
||||||
|
|
||||||
|
# Determine if text is large
|
||||||
|
style = node.get('style', {})
|
||||||
|
font_size = style.get('fontSize', 16)
|
||||||
|
font_weight = style.get('fontWeight', 400)
|
||||||
|
|
||||||
|
is_large_text = font_size >= 18 or (font_size >= 14 and font_weight >= 700)
|
||||||
|
required_ratio = requirements['large_text'] if is_large_text else requirements['normal_text']
|
||||||
|
|
||||||
|
if contrast_ratio < required_ratio:
|
||||||
|
results['issues'].append({
|
||||||
|
'type': 'color_contrast',
|
||||||
|
'severity': 'error' if level == 'AA' else 'warning',
|
||||||
|
'message': f'Insufficient contrast: {contrast_ratio:.1f}:1 (required: {required_ratio}:1)',
|
||||||
|
'node_id': node.get('id'),
|
||||||
|
'node_name': node.get('name', ''),
|
||||||
|
'wcag_criterion': '1.4.3' if level == 'AA' else '1.4.6',
|
||||||
|
'details': {
|
||||||
|
'contrast_ratio': contrast_ratio,
|
||||||
|
'required_ratio': required_ratio,
|
||||||
|
'text_color': self._rgb_to_hex(text_color),
|
||||||
|
'is_large_text': is_large_text
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check children
|
||||||
|
for child in node.get('children', []):
|
||||||
|
check_node_contrast(child)
|
||||||
|
|
||||||
|
if 'document' in file_data:
|
||||||
|
check_node_contrast(file_data['document'])
|
||||||
|
|
||||||
|
def _check_touch_targets(self, file_data: Dict[str, Any], results: Dict[str, Any]):
|
||||||
|
"""Check minimum touch target sizes (WCAG 2.5.5)"""
|
||||||
|
|
||||||
|
min_size = 44 # iOS/WCAG standard
|
||||||
|
|
||||||
|
def check_node_size(node):
|
||||||
|
# Look for interactive elements
|
||||||
|
node_name = node.get('name', '').lower()
|
||||||
|
node_type = node.get('type', '')
|
||||||
|
|
||||||
|
is_interactive = (
|
||||||
|
'button' in node_name or
|
||||||
|
'link' in node_name or
|
||||||
|
node_type in ['COMPONENT', 'INSTANCE'] and
|
||||||
|
any(keyword in node_name for keyword in ['btn', 'tap', 'click', 'interactive'])
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_interactive:
|
||||||
|
bounds = node.get('absoluteBoundingBox', {})
|
||||||
|
width = bounds.get('width', 0)
|
||||||
|
height = bounds.get('height', 0)
|
||||||
|
|
||||||
|
if width < min_size or height < min_size:
|
||||||
|
results['issues'].append({
|
||||||
|
'type': 'touch_target',
|
||||||
|
'severity': 'warning',
|
||||||
|
'message': f'Touch target too small: {width:.0f}×{height:.0f}px (minimum: {min_size}×{min_size}px)',
|
||||||
|
'node_id': node.get('id'),
|
||||||
|
'node_name': node.get('name', ''),
|
||||||
|
'wcag_criterion': '2.5.5',
|
||||||
|
'details': {
|
||||||
|
'width': width,
|
||||||
|
'height': height,
|
||||||
|
'min_size': min_size
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check children
|
||||||
|
for child in node.get('children', []):
|
||||||
|
check_node_size(child)
|
||||||
|
|
||||||
|
if 'document' in file_data:
|
||||||
|
check_node_size(file_data['document'])
|
||||||
|
|
||||||
|
def _check_text_sizing(self, file_data: Dict[str, Any], results: Dict[str, Any]):
|
||||||
|
"""Check minimum text sizes for readability"""
|
||||||
|
|
||||||
|
min_size = 12 # Minimum readable size
|
||||||
|
recommended_size = 16 # Recommended for body text
|
||||||
|
|
||||||
|
def check_text_size(node):
|
||||||
|
if node.get('type') == 'TEXT':
|
||||||
|
style = node.get('style', {})
|
||||||
|
font_size = style.get('fontSize', 16)
|
||||||
|
|
||||||
|
if font_size < min_size:
|
||||||
|
results['issues'].append({
|
||||||
|
'type': 'text_size',
|
||||||
|
'severity': 'error',
|
||||||
|
'message': f'Text too small: {font_size}px (minimum: {min_size}px)',
|
||||||
|
'node_id': node.get('id'),
|
||||||
|
'node_name': node.get('name', ''),
|
||||||
|
'wcag_criterion': '1.4.4',
|
||||||
|
'details': {
|
||||||
|
'font_size': font_size,
|
||||||
|
'min_size': min_size,
|
||||||
|
'characters': node.get('characters', '')[:50]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
elif font_size < recommended_size:
|
||||||
|
results['issues'].append({
|
||||||
|
'type': 'text_size',
|
||||||
|
'severity': 'info',
|
||||||
|
'message': f'Text smaller than recommended: {font_size}px (recommended: {recommended_size}px)',
|
||||||
|
'node_id': node.get('id'),
|
||||||
|
'node_name': node.get('name', ''),
|
||||||
|
'wcag_criterion': '1.4.4',
|
||||||
|
'details': {
|
||||||
|
'font_size': font_size,
|
||||||
|
'recommended_size': recommended_size
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check children
|
||||||
|
for child in node.get('children', []):
|
||||||
|
check_text_size(child)
|
||||||
|
|
||||||
|
if 'document' in file_data:
|
||||||
|
check_text_size(file_data['document'])
|
||||||
|
|
||||||
|
def _check_focus_indicators(self, file_data: Dict[str, Any], results: Dict[str, Any]):
|
||||||
|
"""Check for focus indicators on interactive elements"""
|
||||||
|
|
||||||
|
def check_focus_states(node):
|
||||||
|
node_name = node.get('name', '').lower()
|
||||||
|
node_type = node.get('type', '')
|
||||||
|
|
||||||
|
is_interactive = (
|
||||||
|
'button' in node_name or
|
||||||
|
'link' in node_name or
|
||||||
|
'input' in node_name or
|
||||||
|
node_type in ['COMPONENT', 'INSTANCE']
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_interactive:
|
||||||
|
# Check for focus-related effects or states
|
||||||
|
effects = node.get('effects', [])
|
||||||
|
has_focus_indicator = any(
|
||||||
|
'focus' in str(effect).lower() or
|
||||||
|
effect.get('type') == 'DROP_SHADOW'
|
||||||
|
for effect in effects
|
||||||
|
)
|
||||||
|
|
||||||
|
if not has_focus_indicator:
|
||||||
|
results['issues'].append({
|
||||||
|
'type': 'focus_indicator',
|
||||||
|
'severity': 'info',
|
||||||
|
'message': 'Interactive element may need focus indicator',
|
||||||
|
'node_id': node.get('id'),
|
||||||
|
'node_name': node.get('name', ''),
|
||||||
|
'wcag_criterion': '2.4.7',
|
||||||
|
'details': {
|
||||||
|
'suggestion': 'Add visible focus state for keyboard navigation'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check children
|
||||||
|
for child in node.get('children', []):
|
||||||
|
check_focus_states(child)
|
||||||
|
|
||||||
|
if 'document' in file_data:
|
||||||
|
check_focus_states(file_data['document'])
|
||||||
|
|
||||||
|
def _calculate_contrast_ratio(self, color1: Dict[str, float], color2: Dict[str, float]) -> float:
|
||||||
|
"""Calculate WCAG contrast ratio between two colors"""
|
||||||
|
|
||||||
|
def get_luminance(color):
|
||||||
|
def linearize(val):
|
||||||
|
if val <= 0.03928:
|
||||||
|
return val / 12.92
|
||||||
|
else:
|
||||||
|
return pow((val + 0.055) / 1.055, 2.4)
|
||||||
|
|
||||||
|
r = linearize(color.get('r', 0))
|
||||||
|
g = linearize(color.get('g', 0))
|
||||||
|
b = linearize(color.get('b', 0))
|
||||||
|
|
||||||
|
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
||||||
|
|
||||||
|
lum1 = get_luminance(color1)
|
||||||
|
lum2 = get_luminance(color2)
|
||||||
|
|
||||||
|
lighter = max(lum1, lum2)
|
||||||
|
darker = min(lum1, lum2)
|
||||||
|
|
||||||
|
return (lighter + 0.05) / (darker + 0.05)
|
||||||
|
|
||||||
|
def _rgb_to_hex(self, color: Dict[str, float]) -> str:
|
||||||
|
"""Convert RGB color to hex string"""
|
||||||
|
r = int(color.get('r', 0) * 255)
|
||||||
|
g = int(color.get('g', 0) * 255)
|
||||||
|
b = int(color.get('b', 0) * 255)
|
||||||
|
return f"#{r:02x}{g:02x}{b:02x}"
|
||||||
|
|
||||||
|
def _calculate_compliance_score(self, results: Dict[str, Any]) -> int:
|
||||||
|
"""Calculate overall compliance score (0-100)"""
|
||||||
|
|
||||||
|
error_count = len([i for i in results['issues'] if i['severity'] == 'error'])
|
||||||
|
warning_count = len([i for i in results['issues'] if i['severity'] == 'warning'])
|
||||||
|
info_count = len([i for i in results['issues'] if i['severity'] == 'info'])
|
||||||
|
|
||||||
|
# Scoring: errors are -10 points, warnings -3 points, info -1 point
|
||||||
|
penalty = error_count * 10 + warning_count * 3 + info_count * 1
|
||||||
|
score = max(0, 100 - penalty)
|
||||||
|
|
||||||
|
return score
|
||||||
|
|
||||||
|
def _generate_summary(self, results: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Generate summary of accessibility results"""
|
||||||
|
|
||||||
|
issues_by_type = {}
|
||||||
|
issues_by_severity = {'error': 0, 'warning': 0, 'info': 0}
|
||||||
|
|
||||||
|
for issue in results['issues']:
|
||||||
|
issue_type = issue['type']
|
||||||
|
severity = issue['severity']
|
||||||
|
|
||||||
|
issues_by_type[issue_type] = issues_by_type.get(issue_type, 0) + 1
|
||||||
|
issues_by_severity[severity] += 1
|
||||||
|
|
||||||
|
compliance_level = 'FAIL'
|
||||||
|
if issues_by_severity['error'] == 0:
|
||||||
|
if issues_by_severity['warning'] == 0:
|
||||||
|
compliance_level = 'AAA'
|
||||||
|
elif issues_by_severity['warning'] <= 2:
|
||||||
|
compliance_level = 'AA'
|
||||||
|
else:
|
||||||
|
compliance_level = 'A'
|
||||||
|
|
||||||
|
return {
|
||||||
|
'total_issues': len(results['issues']),
|
||||||
|
'issues_by_type': issues_by_type,
|
||||||
|
'issues_by_severity': issues_by_severity,
|
||||||
|
'compliance_level': compliance_level,
|
||||||
|
'score': results['compliance_score']
|
||||||
|
}
|
||||||
|
|
||||||
|
def generate_accessibility_report(self, results: Dict[str, Any], output_path: str = None) -> str:
|
||||||
|
"""Generate detailed accessibility report"""
|
||||||
|
|
||||||
|
if not output_path:
|
||||||
|
output_path = f"accessibility-report-{int(time.time())}.html"
|
||||||
|
|
||||||
|
html_report = self._create_accessibility_html_report(results)
|
||||||
|
|
||||||
|
with open(output_path, 'w') as f:
|
||||||
|
f.write(html_report)
|
||||||
|
|
||||||
|
print(f"Accessibility report generated: {output_path}")
|
||||||
|
return output_path
|
||||||
|
|
||||||
|
def _create_accessibility_html_report(self, results: Dict[str, Any]) -> str:
|
||||||
|
"""Create comprehensive HTML accessibility report"""
|
||||||
|
|
||||||
|
# Color coding for compliance levels
|
||||||
|
level_colors = {
|
||||||
|
'AAA': '#28a745',
|
||||||
|
'AA': '#17a2b8',
|
||||||
|
'A': '#ffc107',
|
||||||
|
'FAIL': '#dc3545'
|
||||||
|
}
|
||||||
|
|
||||||
|
level_color = level_colors.get(results['summary']['compliance_level'], '#6c757d')
|
||||||
|
|
||||||
|
html = f"""<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Accessibility Report - {results['file_name']}</title>
|
||||||
|
<style>
|
||||||
|
body {{
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
margin: 40px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
}}
|
||||||
|
.header {{
|
||||||
|
border-bottom: 3px solid {level_color};
|
||||||
|
padding-bottom: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}}
|
||||||
|
.compliance-badge {{
|
||||||
|
display: inline-block;
|
||||||
|
background: {level_color};
|
||||||
|
color: white;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 18px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}}
|
||||||
|
.score {{
|
||||||
|
font-size: 48px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: {level_color};
|
||||||
|
}}
|
||||||
|
.summary {{
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border-left: 5px solid {level_color};
|
||||||
|
}}
|
||||||
|
.issue {{
|
||||||
|
margin-bottom: 25px;
|
||||||
|
padding: 20px;
|
||||||
|
border-left: 4px solid #ddd;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}}
|
||||||
|
.error {{ border-left-color: #dc3545; background: #f8d7da; }}
|
||||||
|
.warning {{ border-left-color: #ffc107; background: #fff3cd; }}
|
||||||
|
.info {{ border-left-color: #17a2b8; background: #d1ecf1; }}
|
||||||
|
.wcag-criterion {{
|
||||||
|
background: #e9ecef;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}}
|
||||||
|
.node-info {{
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}}
|
||||||
|
.stats {{
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}}
|
||||||
|
.stat {{
|
||||||
|
background: white;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}}
|
||||||
|
.stat-number {{
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: {level_color};
|
||||||
|
}}
|
||||||
|
.recommendations {{
|
||||||
|
background: #e7f3ff;
|
||||||
|
border: 1px solid #b8daff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 30px 0;
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1>🔍 Accessibility Report</h1>
|
||||||
|
<p><strong>File:</strong> {results['file_name']}</p>
|
||||||
|
<p><strong>WCAG Level:</strong> {results['wcag_level']}</p>
|
||||||
|
<p><strong>Generated:</strong> {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(results['timestamp']))}</p>
|
||||||
|
<div class="compliance-badge">
|
||||||
|
WCAG {results['summary']['compliance_level']} Compliance
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="summary">
|
||||||
|
<h2>📊 Summary</h2>
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-number">{results['summary']['score']}</div>
|
||||||
|
<div>Score</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-number">{results['summary']['total_issues']}</div>
|
||||||
|
<div>Total Issues</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-number">{results['summary']['issues_by_severity']['error']}</div>
|
||||||
|
<div>Errors</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-number">{results['summary']['issues_by_severity']['warning']}</div>
|
||||||
|
<div>Warnings</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
if results['issues']:
|
||||||
|
html += "<h2>🐛 Issues Found</h2>\n"
|
||||||
|
|
||||||
|
for issue in results['issues']:
|
||||||
|
severity_class = issue['severity']
|
||||||
|
html += f"""
|
||||||
|
<div class="issue {severity_class}">
|
||||||
|
<h3>{issue['type'].replace('_', ' ').title()}: {issue['message']}</h3>
|
||||||
|
<span class="wcag-criterion">WCAG {issue['wcag_criterion']}</span>
|
||||||
|
<div class="node-info">
|
||||||
|
<strong>Element:</strong> {issue.get('node_name', 'N/A')}
|
||||||
|
(ID: {issue.get('node_id', 'N/A')})
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
if 'details' in issue and issue['details']:
|
||||||
|
html += "<div style='margin-top: 10px;'><strong>Details:</strong><ul>"
|
||||||
|
for key, value in issue['details'].items():
|
||||||
|
html += f"<li><strong>{key.replace('_', ' ').title()}:</strong> {value}</li>"
|
||||||
|
html += "</ul></div>"
|
||||||
|
|
||||||
|
html += "</div>\n"
|
||||||
|
else:
|
||||||
|
html += """
|
||||||
|
<div class="recommendations">
|
||||||
|
<h2>🎉 Excellent Work!</h2>
|
||||||
|
<p>No accessibility issues found in this design. This indicates strong adherence to WCAG guidelines.</p>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
html += """
|
||||||
|
<div class="recommendations">
|
||||||
|
<h2>💡 Recommendations</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Manual Testing:</strong> Automated checks catch many issues, but manual testing with assistive technologies is still essential.</li>
|
||||||
|
<li><strong>User Testing:</strong> Include users with disabilities in your testing process.</li>
|
||||||
|
<li><strong>Regular Audits:</strong> Run accessibility checks throughout the design process, not just at the end.</li>
|
||||||
|
<li><strong>Design System:</strong> Build accessibility into your component library to prevent issues.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 40px; padding-top: 20px; border-top: 1px solid #dee2e6; color: #6c757d; font-size: 14px;">
|
||||||
|
<p>Generated by Figma Accessibility Checker | Learn more about WCAG at <a href="https://www.w3.org/WAI/WCAG21/quickref/">WCAG Quick Reference</a></p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
return html
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""CLI interface for accessibility checking"""
|
||||||
|
parser = argparse.ArgumentParser(description='Figma Accessibility Checker')
|
||||||
|
parser.add_argument('file_key', help='Figma file key or URL')
|
||||||
|
parser.add_argument('--level', choices=['AA', 'AAA'], default='AA', help='WCAG compliance level')
|
||||||
|
parser.add_argument('--output', help='Output file for accessibility report')
|
||||||
|
parser.add_argument('--format', choices=['json', 'html'], default='json', help='Output format')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = FigmaClient()
|
||||||
|
checker = AccessibilityChecker(client)
|
||||||
|
|
||||||
|
file_key = client.parse_file_url(args.file_key)
|
||||||
|
results = checker.check_wcag_compliance(file_key, args.level)
|
||||||
|
|
||||||
|
if args.format == 'html':
|
||||||
|
output_path = args.output or f"accessibility-report-{file_key}.html"
|
||||||
|
checker.generate_accessibility_report(results, output_path)
|
||||||
|
else:
|
||||||
|
output_content = json.dumps(results, indent=2)
|
||||||
|
|
||||||
|
if args.output:
|
||||||
|
with open(args.output, 'w') as f:
|
||||||
|
f.write(output_content)
|
||||||
|
print(f"Accessibility results saved to {args.output}")
|
||||||
|
else:
|
||||||
|
print(output_content)
|
||||||
|
|
||||||
|
# Print summary
|
||||||
|
print(f"\n🔍 Accessibility Summary:")
|
||||||
|
print(f" Score: {results['summary']['score']}/100")
|
||||||
|
print(f" Compliance Level: WCAG {results['summary']['compliance_level']}")
|
||||||
|
print(f" Total Issues: {results['summary']['total_issues']}")
|
||||||
|
print(f" Errors: {results['summary']['issues_by_severity']['error']}")
|
||||||
|
print(f" Warnings: {results['summary']['issues_by_severity']['warning']}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
559
scripts/export_manager.py
Normal file
559
scripts/export_manager.py
Normal file
@@ -0,0 +1,559 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Figma Export Manager - Batch asset export with intelligent organization
|
||||||
|
Handles multiple formats, naming conventions, and export workflows.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Optional, Union, Any
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
import argparse
|
||||||
|
import time
|
||||||
|
try:
|
||||||
|
from figma_client import FigmaClient
|
||||||
|
except ImportError:
|
||||||
|
# Handle case where script is run directly
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
sys.path.append(str(Path(__file__).parent))
|
||||||
|
from figma_client import FigmaClient
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ExportConfig:
|
||||||
|
"""Configuration for export operations"""
|
||||||
|
formats: List[str] = field(default_factory=lambda: ['png'])
|
||||||
|
scales: List[float] = field(default_factory=lambda: [1.0])
|
||||||
|
output_dir: str = './figma-exports'
|
||||||
|
naming_pattern: str = '{name}_{id}.{format}'
|
||||||
|
create_manifest: bool = True
|
||||||
|
skip_existing: bool = False
|
||||||
|
max_concurrent: int = 5
|
||||||
|
organize_by_format: bool = True
|
||||||
|
|
||||||
|
class ExportManager:
|
||||||
|
"""Professional-grade Figma asset export manager"""
|
||||||
|
|
||||||
|
def __init__(self, figma_client: FigmaClient, config: ExportConfig = None):
|
||||||
|
self.client = figma_client
|
||||||
|
self.config = config or ExportConfig()
|
||||||
|
|
||||||
|
# Create output directory
|
||||||
|
Path(self.config.output_dir).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
def export_frames(self, file_key: str, frame_ids: List[str] = None,
|
||||||
|
frame_names: List[str] = None) -> Dict[str, Any]:
|
||||||
|
"""Export all frames or specific frames from a file"""
|
||||||
|
|
||||||
|
# Get file data to identify frames
|
||||||
|
file_data = self.client.get_file(file_key)
|
||||||
|
|
||||||
|
if not frame_ids and not frame_names:
|
||||||
|
# Export all frames
|
||||||
|
frame_nodes = self._find_frames(file_data)
|
||||||
|
else:
|
||||||
|
# Filter specific frames
|
||||||
|
all_frames = self._find_frames(file_data)
|
||||||
|
frame_nodes = []
|
||||||
|
|
||||||
|
for frame in all_frames:
|
||||||
|
if frame_ids and frame['id'] in frame_ids:
|
||||||
|
frame_nodes.append(frame)
|
||||||
|
elif frame_names and frame['name'] in frame_names:
|
||||||
|
frame_nodes.append(frame)
|
||||||
|
|
||||||
|
if not frame_nodes:
|
||||||
|
print("No frames found to export")
|
||||||
|
return {'exported': 0, 'files': []}
|
||||||
|
|
||||||
|
print(f"Found {len(frame_nodes)} frames to export")
|
||||||
|
return self._export_nodes(file_key, frame_nodes)
|
||||||
|
|
||||||
|
def export_components(self, file_key: str, component_names: List[str] = None) -> Dict[str, Any]:
|
||||||
|
"""Export all components or specific components from a file"""
|
||||||
|
|
||||||
|
file_data = self.client.get_file(file_key)
|
||||||
|
component_nodes = self._find_components(file_data)
|
||||||
|
|
||||||
|
if component_names:
|
||||||
|
component_nodes = [c for c in component_nodes if c['name'] in component_names]
|
||||||
|
|
||||||
|
if not component_nodes:
|
||||||
|
print("No components found to export")
|
||||||
|
return {'exported': 0, 'files': []}
|
||||||
|
|
||||||
|
print(f"Found {len(component_nodes)} components to export")
|
||||||
|
return self._export_nodes(file_key, component_nodes)
|
||||||
|
|
||||||
|
def export_pages(self, file_key: str, page_names: List[str] = None) -> Dict[str, Any]:
|
||||||
|
"""Export all pages or specific pages as complete images"""
|
||||||
|
|
||||||
|
file_data = self.client.get_file(file_key)
|
||||||
|
|
||||||
|
pages = []
|
||||||
|
for child in file_data.get('document', {}).get('children', []):
|
||||||
|
if child.get('type') == 'CANVAS':
|
||||||
|
if not page_names or child.get('name') in page_names:
|
||||||
|
pages.append(child)
|
||||||
|
|
||||||
|
if not pages:
|
||||||
|
print("No pages found to export")
|
||||||
|
return {'exported': 0, 'files': []}
|
||||||
|
|
||||||
|
print(f"Found {len(pages)} pages to export")
|
||||||
|
return self._export_nodes(file_key, pages)
|
||||||
|
|
||||||
|
def export_custom_selection(self, file_key: str, node_ids: List[str]) -> Dict[str, Any]:
|
||||||
|
"""Export specific nodes by ID"""
|
||||||
|
|
||||||
|
# Get node information
|
||||||
|
nodes_data = self.client.get_file_nodes(file_key, node_ids)
|
||||||
|
|
||||||
|
if 'nodes' not in nodes_data:
|
||||||
|
print("No nodes found with provided IDs")
|
||||||
|
return {'exported': 0, 'files': []}
|
||||||
|
|
||||||
|
nodes = []
|
||||||
|
for node_id, node_info in nodes_data['nodes'].items():
|
||||||
|
if 'document' in node_info:
|
||||||
|
node = node_info['document']
|
||||||
|
node['id'] = node_id # Ensure ID is present
|
||||||
|
nodes.append(node)
|
||||||
|
|
||||||
|
print(f"Found {len(nodes)} nodes to export")
|
||||||
|
return self._export_nodes(file_key, nodes)
|
||||||
|
|
||||||
|
def export_design_tokens(self, file_key: str, output_format: str = 'json') -> str:
|
||||||
|
"""Export design tokens (colors, typography, effects) in various formats"""
|
||||||
|
|
||||||
|
file_data = self.client.get_file(file_key)
|
||||||
|
|
||||||
|
# Extract design tokens
|
||||||
|
tokens = {
|
||||||
|
'colors': self._extract_color_tokens(file_data),
|
||||||
|
'typography': self._extract_typography_tokens(file_data),
|
||||||
|
'effects': self._extract_effect_tokens(file_data),
|
||||||
|
'spacing': self._extract_spacing_tokens(file_data)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Format output
|
||||||
|
if output_format == 'css':
|
||||||
|
output_content = self._tokens_to_css(tokens)
|
||||||
|
output_file = Path(self.config.output_dir) / 'design-tokens.css'
|
||||||
|
elif output_format == 'scss':
|
||||||
|
output_content = self._tokens_to_scss(tokens)
|
||||||
|
output_file = Path(self.config.output_dir) / 'design-tokens.scss'
|
||||||
|
elif output_format == 'js':
|
||||||
|
output_content = self._tokens_to_js(tokens)
|
||||||
|
output_file = Path(self.config.output_dir) / 'design-tokens.js'
|
||||||
|
else: # json
|
||||||
|
output_content = json.dumps(tokens, indent=2)
|
||||||
|
output_file = Path(self.config.output_dir) / 'design-tokens.json'
|
||||||
|
|
||||||
|
# Write output file
|
||||||
|
with open(output_file, 'w') as f:
|
||||||
|
f.write(output_content)
|
||||||
|
|
||||||
|
print(f"Design tokens exported to {output_file}")
|
||||||
|
return str(output_file)
|
||||||
|
|
||||||
|
def create_client_package(self, file_key: str, package_name: str = None) -> str:
|
||||||
|
"""Create a complete client delivery package with all assets"""
|
||||||
|
|
||||||
|
if not package_name:
|
||||||
|
file_data = self.client.get_file(file_key)
|
||||||
|
package_name = file_data.get('name', 'figma-package').replace(' ', '-').lower()
|
||||||
|
|
||||||
|
package_dir = Path(self.config.output_dir) / package_name
|
||||||
|
package_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Export all asset types
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
# 1. Export all frames
|
||||||
|
self.config.output_dir = str(package_dir / 'frames')
|
||||||
|
Path(self.config.output_dir).mkdir(exist_ok=True)
|
||||||
|
results['frames'] = self.export_frames(file_key)
|
||||||
|
|
||||||
|
# 2. Export all components
|
||||||
|
self.config.output_dir = str(package_dir / 'components')
|
||||||
|
Path(self.config.output_dir).mkdir(exist_ok=True)
|
||||||
|
results['components'] = self.export_components(file_key)
|
||||||
|
|
||||||
|
# 3. Export design tokens
|
||||||
|
self.config.output_dir = str(package_dir)
|
||||||
|
results['tokens'] = {
|
||||||
|
'json': self.export_design_tokens(file_key, 'json'),
|
||||||
|
'css': self.export_design_tokens(file_key, 'css'),
|
||||||
|
'scss': self.export_design_tokens(file_key, 'scss')
|
||||||
|
}
|
||||||
|
|
||||||
|
# 4. Create documentation
|
||||||
|
doc_file = package_dir / 'README.md'
|
||||||
|
self._create_package_documentation(file_key, doc_file, results)
|
||||||
|
|
||||||
|
print(f"Client package created at {package_dir}")
|
||||||
|
return str(package_dir)
|
||||||
|
|
||||||
|
def _export_nodes(self, file_key: str, nodes: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||||
|
"""Export nodes with all configured formats and scales"""
|
||||||
|
|
||||||
|
exported_files = []
|
||||||
|
total_exports = len(nodes) * len(self.config.formats) * len(self.config.scales)
|
||||||
|
current_export = 0
|
||||||
|
|
||||||
|
for node in nodes:
|
||||||
|
node_id = node['id']
|
||||||
|
node_name = self._sanitize_filename(node.get('name', 'untitled'))
|
||||||
|
|
||||||
|
for format in self.config.formats:
|
||||||
|
for scale in self.config.scales:
|
||||||
|
current_export += 1
|
||||||
|
print(f"Exporting {current_export}/{total_exports}: {node_name} ({format} @ {scale}x)")
|
||||||
|
|
||||||
|
# Get export URLs from Figma
|
||||||
|
try:
|
||||||
|
export_data = self.client.export_images(
|
||||||
|
file_key, [node_id],
|
||||||
|
format=format, scale=scale
|
||||||
|
)
|
||||||
|
|
||||||
|
if 'images' in export_data and node_id in export_data['images']:
|
||||||
|
image_url = export_data['images'][node_id]
|
||||||
|
|
||||||
|
if image_url:
|
||||||
|
# Generate filename
|
||||||
|
filename = self.config.naming_pattern.format(
|
||||||
|
name=node_name,
|
||||||
|
id=node_id,
|
||||||
|
format=format,
|
||||||
|
scale=f'{scale}x' if scale != 1.0 else ''
|
||||||
|
)
|
||||||
|
|
||||||
|
# Organize by format if configured
|
||||||
|
if self.config.organize_by_format:
|
||||||
|
format_dir = Path(self.config.output_dir) / format
|
||||||
|
format_dir.mkdir(exist_ok=True)
|
||||||
|
output_path = format_dir / filename
|
||||||
|
else:
|
||||||
|
output_path = Path(self.config.output_dir) / filename
|
||||||
|
|
||||||
|
# Skip if file exists and skip_existing is True
|
||||||
|
if self.config.skip_existing and output_path.exists():
|
||||||
|
print(f" Skipping existing file: {output_path}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Download the image
|
||||||
|
self.client.download_image(str(image_url), str(output_path))
|
||||||
|
|
||||||
|
exported_files.append({
|
||||||
|
'path': str(output_path),
|
||||||
|
'node_id': node_id,
|
||||||
|
'node_name': node_name,
|
||||||
|
'format': format,
|
||||||
|
'scale': scale,
|
||||||
|
'url': image_url
|
||||||
|
})
|
||||||
|
|
||||||
|
print(f" Saved: {output_path}")
|
||||||
|
else:
|
||||||
|
print(f" Warning: No image URL returned for {node_name}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Error exporting {node_name}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Rate limiting
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# Create manifest file
|
||||||
|
if self.config.create_manifest:
|
||||||
|
manifest_path = Path(self.config.output_dir) / 'export-manifest.json'
|
||||||
|
manifest_data = {
|
||||||
|
'exported_at': time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
'file_key': file_key,
|
||||||
|
'total_files': len(exported_files),
|
||||||
|
'config': {
|
||||||
|
'formats': self.config.formats,
|
||||||
|
'scales': self.config.scales,
|
||||||
|
'naming_pattern': self.config.naming_pattern
|
||||||
|
},
|
||||||
|
'files': exported_files
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(manifest_path, 'w') as f:
|
||||||
|
json.dump(manifest_data, f, indent=2)
|
||||||
|
|
||||||
|
print(f"Manifest created: {manifest_path}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
'exported': len(exported_files),
|
||||||
|
'files': exported_files
|
||||||
|
}
|
||||||
|
|
||||||
|
def _find_frames(self, file_data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
|
"""Find all frames in the file"""
|
||||||
|
frames = []
|
||||||
|
|
||||||
|
def traverse_node(node):
|
||||||
|
if node.get('type') == 'FRAME':
|
||||||
|
frames.append(node)
|
||||||
|
|
||||||
|
for child in node.get('children', []):
|
||||||
|
traverse_node(child)
|
||||||
|
|
||||||
|
if 'document' in file_data:
|
||||||
|
traverse_node(file_data['document'])
|
||||||
|
|
||||||
|
return frames
|
||||||
|
|
||||||
|
def _find_components(self, file_data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
|
"""Find all components in the file"""
|
||||||
|
components = []
|
||||||
|
|
||||||
|
def traverse_node(node):
|
||||||
|
if node.get('type') == 'COMPONENT':
|
||||||
|
components.append(node)
|
||||||
|
|
||||||
|
for child in node.get('children', []):
|
||||||
|
traverse_node(child)
|
||||||
|
|
||||||
|
if 'document' in file_data:
|
||||||
|
traverse_node(file_data['document'])
|
||||||
|
|
||||||
|
return components
|
||||||
|
|
||||||
|
def _extract_color_tokens(self, file_data: Dict[str, Any]) -> Dict[str, str]:
|
||||||
|
"""Extract color design tokens from file styles"""
|
||||||
|
colors = {}
|
||||||
|
|
||||||
|
for style_id, style in file_data.get('styles', {}).items():
|
||||||
|
if style.get('styleType') == 'FILL':
|
||||||
|
name = style.get('name', '').replace('/', '-').lower()
|
||||||
|
# This would need to be enhanced with actual color values
|
||||||
|
# from the style definition
|
||||||
|
colors[name] = f"#{style_id[:6]}" # Placeholder
|
||||||
|
|
||||||
|
return colors
|
||||||
|
|
||||||
|
def _extract_typography_tokens(self, file_data: Dict[str, Any]) -> Dict[str, Dict]:
|
||||||
|
"""Extract typography design tokens"""
|
||||||
|
typography = {}
|
||||||
|
|
||||||
|
for style_id, style in file_data.get('styles', {}).items():
|
||||||
|
if style.get('styleType') == 'TEXT':
|
||||||
|
name = style.get('name', '').replace('/', '-').lower()
|
||||||
|
typography[name] = {
|
||||||
|
'fontSize': '16px', # Placeholder - would need actual values
|
||||||
|
'fontWeight': '400',
|
||||||
|
'lineHeight': '1.5',
|
||||||
|
'fontFamily': 'Inter'
|
||||||
|
}
|
||||||
|
|
||||||
|
return typography
|
||||||
|
|
||||||
|
def _extract_effect_tokens(self, file_data: Dict[str, Any]) -> Dict[str, str]:
|
||||||
|
"""Extract effect design tokens (shadows, etc.)"""
|
||||||
|
effects = {}
|
||||||
|
|
||||||
|
for style_id, style in file_data.get('styles', {}).items():
|
||||||
|
if style.get('styleType') == 'EFFECT':
|
||||||
|
name = style.get('name', '').replace('/', '-').lower()
|
||||||
|
effects[name] = "0 2px 4px rgba(0,0,0,0.1)" # Placeholder
|
||||||
|
|
||||||
|
return effects
|
||||||
|
|
||||||
|
def _extract_spacing_tokens(self, file_data: Dict[str, Any]) -> Dict[str, str]:
|
||||||
|
"""Extract spacing tokens from layout patterns"""
|
||||||
|
# This would analyze common spacing patterns in the design
|
||||||
|
return {
|
||||||
|
'xs': '4px',
|
||||||
|
'sm': '8px',
|
||||||
|
'md': '16px',
|
||||||
|
'lg': '24px',
|
||||||
|
'xl': '32px'
|
||||||
|
}
|
||||||
|
|
||||||
|
def _tokens_to_css(self, tokens: Dict[str, Any]) -> str:
|
||||||
|
"""Convert tokens to CSS custom properties"""
|
||||||
|
css_content = ":root {\n"
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
for name, value in tokens['colors'].items():
|
||||||
|
css_content += f" --color-{name}: {value};\n"
|
||||||
|
|
||||||
|
# Typography
|
||||||
|
for name, values in tokens['typography'].items():
|
||||||
|
for prop, value in values.items():
|
||||||
|
css_content += f" --{name}-{prop.lower()}: {value};\n"
|
||||||
|
|
||||||
|
# Effects
|
||||||
|
for name, value in tokens['effects'].items():
|
||||||
|
css_content += f" --effect-{name}: {value};\n"
|
||||||
|
|
||||||
|
# Spacing
|
||||||
|
for name, value in tokens['spacing'].items():
|
||||||
|
css_content += f" --spacing-{name}: {value};\n"
|
||||||
|
|
||||||
|
css_content += "}\n"
|
||||||
|
return css_content
|
||||||
|
|
||||||
|
def _tokens_to_scss(self, tokens: Dict[str, Any]) -> str:
|
||||||
|
"""Convert tokens to SCSS variables"""
|
||||||
|
scss_content = "// Design Tokens\n\n"
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
scss_content += "// Colors\n"
|
||||||
|
for name, value in tokens['colors'].items():
|
||||||
|
scss_content += f"${name.replace('-', '_')}: {value};\n"
|
||||||
|
|
||||||
|
scss_content += "\n// Typography\n"
|
||||||
|
for name, values in tokens['typography'].items():
|
||||||
|
for prop, value in values.items():
|
||||||
|
scss_content += f"${name.replace('-', '_')}_{prop.lower()}: {value};\n"
|
||||||
|
|
||||||
|
return scss_content
|
||||||
|
|
||||||
|
def _tokens_to_js(self, tokens: Dict[str, Any]) -> str:
|
||||||
|
"""Convert tokens to JavaScript/JSON module"""
|
||||||
|
return f"export const designTokens = {json.dumps(tokens, indent=2)};\n\nexport default designTokens;\n"
|
||||||
|
|
||||||
|
def _sanitize_filename(self, name: str) -> str:
|
||||||
|
"""Convert name to safe filename"""
|
||||||
|
# Remove/replace invalid characters
|
||||||
|
safe_name = "".join(c for c in name if c.isalnum() or c in (' ', '-', '_'))
|
||||||
|
safe_name = safe_name.strip().replace(' ', '-').lower()
|
||||||
|
return safe_name or 'untitled'
|
||||||
|
|
||||||
|
def _create_package_documentation(self, file_key: str, doc_path: Path, results: Dict[str, Any]):
|
||||||
|
"""Create documentation for the exported package"""
|
||||||
|
|
||||||
|
file_data = self.client.get_file(file_key)
|
||||||
|
|
||||||
|
doc_content = f"""# {file_data.get('name', 'Figma Export')}
|
||||||
|
|
||||||
|
Exported from Figma on {time.strftime('%Y-%m-%d %H:%M:%S')}
|
||||||
|
|
||||||
|
## File Information
|
||||||
|
- **File Key**: {file_key}
|
||||||
|
- **Last Modified**: {file_data.get('lastModified', 'Unknown')}
|
||||||
|
- **Version**: {file_data.get('version', 'Unknown')}
|
||||||
|
|
||||||
|
## Package Contents
|
||||||
|
|
||||||
|
### Frames ({results.get('frames', {}).get('exported', 0)} files)
|
||||||
|
All page frames exported in configured formats
|
||||||
|
Location: `./frames/`
|
||||||
|
|
||||||
|
### Components ({results.get('components', {}).get('exported', 0)} files)
|
||||||
|
All reusable components exported for development handoff
|
||||||
|
Location: `./components/`
|
||||||
|
|
||||||
|
### Design Tokens
|
||||||
|
Design system tokens in multiple formats:
|
||||||
|
- `design-tokens.json` - Raw token data
|
||||||
|
- `design-tokens.css` - CSS custom properties
|
||||||
|
- `design-tokens.scss` - SCSS variables
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Web Development
|
||||||
|
```css
|
||||||
|
/* Import CSS tokens */
|
||||||
|
@import './design-tokens.css';
|
||||||
|
|
||||||
|
.my-component {{
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: var(--typography-body-fontsize);
|
||||||
|
}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### React/JavaScript
|
||||||
|
```javascript
|
||||||
|
import tokens from './design-tokens.js';
|
||||||
|
|
||||||
|
const MyComponent = () => (
|
||||||
|
<div style={{{{color: tokens.colors.primary}}}}>
|
||||||
|
Content
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support
|
||||||
|
For questions about this export or design implementation, contact your design team.
|
||||||
|
"""
|
||||||
|
|
||||||
|
with open(doc_path, 'w') as f:
|
||||||
|
f.write(doc_content)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""CLI interface for export operations"""
|
||||||
|
parser = argparse.ArgumentParser(description='Figma Export Manager')
|
||||||
|
parser.add_argument('command', choices=[
|
||||||
|
'export-frames', 'export-components', 'export-pages', 'export-nodes',
|
||||||
|
'export-tokens', 'client-package'
|
||||||
|
])
|
||||||
|
parser.add_argument('file_key', help='Figma file key or URL')
|
||||||
|
parser.add_argument('node_ids', nargs='?', help='Comma-separated node IDs for export-nodes')
|
||||||
|
parser.add_argument('--formats', default='png', help='Export formats (comma-separated)')
|
||||||
|
parser.add_argument('--scales', default='1.0', help='Export scales (comma-separated)')
|
||||||
|
parser.add_argument('--output-dir', default='./figma-exports', help='Output directory')
|
||||||
|
parser.add_argument('--token-format', default='json', choices=['json', 'css', 'scss', 'js'])
|
||||||
|
parser.add_argument('--package-name', help='Name for client package')
|
||||||
|
parser.add_argument('--frame-names', help='Specific frame names to export (comma-separated)')
|
||||||
|
parser.add_argument('--component-names', help='Specific component names to export (comma-separated)')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = FigmaClient()
|
||||||
|
file_key = client.parse_file_url(args.file_key)
|
||||||
|
|
||||||
|
# Configure export settings
|
||||||
|
config = ExportConfig(
|
||||||
|
formats=args.formats.split(','),
|
||||||
|
scales=[float(s) for s in args.scales.split(',')],
|
||||||
|
output_dir=args.output_dir
|
||||||
|
)
|
||||||
|
|
||||||
|
manager = ExportManager(client, config)
|
||||||
|
|
||||||
|
if args.command == 'export-frames':
|
||||||
|
frame_names = args.frame_names.split(',') if args.frame_names else None
|
||||||
|
result = manager.export_frames(file_key, frame_names=frame_names)
|
||||||
|
|
||||||
|
elif args.command == 'export-components':
|
||||||
|
component_names = args.component_names.split(',') if args.component_names else None
|
||||||
|
result = manager.export_components(file_key, component_names=component_names)
|
||||||
|
|
||||||
|
elif args.command == 'export-pages':
|
||||||
|
result = manager.export_pages(file_key)
|
||||||
|
|
||||||
|
elif args.command == 'export-nodes':
|
||||||
|
if not args.node_ids:
|
||||||
|
parser.error('node_ids required for export-nodes command')
|
||||||
|
result = manager.export_custom_selection(file_key, args.node_ids.split(','))
|
||||||
|
|
||||||
|
elif args.command == 'export-tokens':
|
||||||
|
result = manager.export_design_tokens(file_key, args.token_format)
|
||||||
|
print(f"Design tokens exported: {result}")
|
||||||
|
return
|
||||||
|
|
||||||
|
elif args.command == 'client-package':
|
||||||
|
result = manager.create_client_package(file_key, args.package_name)
|
||||||
|
print(f"Client package created: {result}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Export completed: {result['exported']} files exported")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
313
scripts/figma_client.py
Normal file
313
scripts/figma_client.py
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Figma API Client - Complete wrapper for Figma REST API
|
||||||
|
Handles authentication, rate limiting, and all major endpoints.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
|
from typing import Dict, List, Optional, Union, Any
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
import argparse
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FigmaConfig:
|
||||||
|
"""Configuration for Figma API client"""
|
||||||
|
access_token: str
|
||||||
|
base_url: str = "https://api.figma.com/v1"
|
||||||
|
rate_limit_delay: float = 0.5
|
||||||
|
max_retries: int = 3
|
||||||
|
|
||||||
|
class FigmaClient:
|
||||||
|
"""Professional-grade Figma API client with rate limiting and error handling"""
|
||||||
|
|
||||||
|
def __init__(self, access_token: str = None):
|
||||||
|
self.config = FigmaConfig(
|
||||||
|
access_token=access_token or os.getenv('FIGMA_ACCESS_TOKEN')
|
||||||
|
)
|
||||||
|
|
||||||
|
if not self.config.access_token:
|
||||||
|
raise ValueError("Figma access token required. Set FIGMA_ACCESS_TOKEN env var or pass token.")
|
||||||
|
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.session.headers.update({
|
||||||
|
'X-Figma-Token': self.config.access_token,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
})
|
||||||
|
|
||||||
|
def _request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]:
|
||||||
|
"""Make authenticated request with rate limiting and retry logic"""
|
||||||
|
url = f"{self.config.base_url}/{endpoint.lstrip('/')}"
|
||||||
|
|
||||||
|
for attempt in range(self.config.max_retries):
|
||||||
|
try:
|
||||||
|
# Rate limiting
|
||||||
|
time.sleep(self.config.rate_limit_delay)
|
||||||
|
|
||||||
|
response = self.session.request(method, url, **kwargs)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
if response.status_code == 429: # Rate limited
|
||||||
|
wait_time = 2 ** attempt
|
||||||
|
print(f"Rate limited. Waiting {wait_time}s before retry {attempt + 1}/{self.config.max_retries}")
|
||||||
|
time.sleep(wait_time)
|
||||||
|
continue
|
||||||
|
elif response.status_code == 403:
|
||||||
|
raise ValueError("Access denied. Check your Figma token permissions.")
|
||||||
|
elif response.status_code == 404:
|
||||||
|
raise ValueError(f"File or resource not found: {url}")
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
if attempt == self.config.max_retries - 1:
|
||||||
|
raise
|
||||||
|
print(f"Request failed, retrying {attempt + 1}/{self.config.max_retries}: {e}")
|
||||||
|
time.sleep(2 ** attempt)
|
||||||
|
|
||||||
|
# ========== FILE OPERATIONS ==========
|
||||||
|
|
||||||
|
def get_file(self, file_key: str, **params) -> Dict[str, Any]:
|
||||||
|
"""Get complete file data including components and styles"""
|
||||||
|
return self._request('GET', f'/files/{file_key}', params=params)
|
||||||
|
|
||||||
|
def get_file_nodes(self, file_key: str, node_ids: Union[str, List[str]], **params) -> Dict[str, Any]:
|
||||||
|
"""Get specific nodes from a file"""
|
||||||
|
if isinstance(node_ids, list):
|
||||||
|
node_ids = ','.join(node_ids)
|
||||||
|
|
||||||
|
params['ids'] = node_ids
|
||||||
|
return self._request('GET', f'/files/{file_key}/nodes', params=params)
|
||||||
|
|
||||||
|
def get_file_versions(self, file_key: str) -> Dict[str, Any]:
|
||||||
|
"""Get version history for a file"""
|
||||||
|
return self._request('GET', f'/files/{file_key}/versions')
|
||||||
|
|
||||||
|
def get_file_components(self, file_key: str) -> Dict[str, Any]:
|
||||||
|
"""Get all components in a file"""
|
||||||
|
return self._request('GET', f'/files/{file_key}/components')
|
||||||
|
|
||||||
|
def get_file_styles(self, file_key: str) -> Dict[str, Any]:
|
||||||
|
"""Get all styles in a file"""
|
||||||
|
return self._request('GET', f'/files/{file_key}/styles')
|
||||||
|
|
||||||
|
# ========== IMAGE EXPORTS ==========
|
||||||
|
|
||||||
|
def export_images(self, file_key: str, node_ids: Union[str, List[str]],
|
||||||
|
format: str = 'png', scale: float = 1.0, **params) -> Dict[str, Any]:
|
||||||
|
"""Export nodes as images"""
|
||||||
|
if isinstance(node_ids, list):
|
||||||
|
node_ids = ','.join(node_ids)
|
||||||
|
|
||||||
|
params.update({
|
||||||
|
'ids': node_ids,
|
||||||
|
'format': format,
|
||||||
|
'scale': scale
|
||||||
|
})
|
||||||
|
|
||||||
|
return self._request('GET', f'/images/{file_key}', params=params)
|
||||||
|
|
||||||
|
def get_image_fills(self, file_key: str) -> Dict[str, Any]:
|
||||||
|
"""Get image fill metadata from a file"""
|
||||||
|
return self._request('GET', f'/files/{file_key}/images')
|
||||||
|
|
||||||
|
# ========== TEAM & PROJECT OPERATIONS ==========
|
||||||
|
|
||||||
|
def get_team_projects(self, team_id: str) -> Dict[str, Any]:
|
||||||
|
"""Get projects for a team"""
|
||||||
|
return self._request('GET', f'/teams/{team_id}/projects')
|
||||||
|
|
||||||
|
def get_project_files(self, project_id: str) -> Dict[str, Any]:
|
||||||
|
"""Get files in a project"""
|
||||||
|
return self._request('GET', f'/projects/{project_id}/files')
|
||||||
|
|
||||||
|
# ========== COMPONENT & STYLE OPERATIONS ==========
|
||||||
|
|
||||||
|
def get_team_components(self, team_id: str, **params) -> Dict[str, Any]:
|
||||||
|
"""Get team component library"""
|
||||||
|
return self._request('GET', f'/teams/{team_id}/components', params=params)
|
||||||
|
|
||||||
|
def get_component(self, component_key: str) -> Dict[str, Any]:
|
||||||
|
"""Get individual component metadata"""
|
||||||
|
return self._request('GET', f'/components/{component_key}')
|
||||||
|
|
||||||
|
def get_team_styles(self, team_id: str, **params) -> Dict[str, Any]:
|
||||||
|
"""Get team style library"""
|
||||||
|
return self._request('GET', f'/teams/{team_id}/styles', params=params)
|
||||||
|
|
||||||
|
def get_style(self, style_key: str) -> Dict[str, Any]:
|
||||||
|
"""Get individual style metadata"""
|
||||||
|
return self._request('GET', f'/styles/{style_key}')
|
||||||
|
|
||||||
|
# ========== UTILITY METHODS ==========
|
||||||
|
|
||||||
|
def parse_file_url(self, url: str) -> str:
|
||||||
|
"""Extract file key from Figma URL"""
|
||||||
|
# https://www.figma.com/file/ABC123/File-Name
|
||||||
|
if '/file/' in url:
|
||||||
|
return url.split('/file/')[1].split('/')[0]
|
||||||
|
return url # Assume it's already a file key
|
||||||
|
|
||||||
|
def get_user_info(self) -> Dict[str, Any]:
|
||||||
|
"""Get current user information"""
|
||||||
|
return self._request('GET', '/me')
|
||||||
|
|
||||||
|
def download_image(self, image_url: str, output_path: str) -> str:
|
||||||
|
"""Download image from Figma CDN"""
|
||||||
|
response = requests.get(image_url)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
with open(output_path, 'wb') as f:
|
||||||
|
f.write(response.content)
|
||||||
|
|
||||||
|
return output_path
|
||||||
|
|
||||||
|
# ========== ANALYSIS HELPERS ==========
|
||||||
|
|
||||||
|
def extract_colors(self, file_data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
|
"""Extract all colors used in a file"""
|
||||||
|
colors = []
|
||||||
|
|
||||||
|
def traverse_node(node):
|
||||||
|
if 'fills' in node:
|
||||||
|
for fill in node.get('fills', []):
|
||||||
|
if fill.get('type') == 'SOLID':
|
||||||
|
color = fill.get('color', {})
|
||||||
|
if color:
|
||||||
|
colors.append({
|
||||||
|
'r': color.get('r', 0),
|
||||||
|
'g': color.get('g', 0),
|
||||||
|
'b': color.get('b', 0),
|
||||||
|
'a': color.get('a', 1),
|
||||||
|
'node_id': node.get('id'),
|
||||||
|
'node_name': node.get('name', '')
|
||||||
|
})
|
||||||
|
|
||||||
|
for child in node.get('children', []):
|
||||||
|
traverse_node(child)
|
||||||
|
|
||||||
|
if 'document' in file_data:
|
||||||
|
traverse_node(file_data['document'])
|
||||||
|
|
||||||
|
return colors
|
||||||
|
|
||||||
|
def extract_text_styles(self, file_data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
|
"""Extract all text styles used in a file"""
|
||||||
|
text_styles = []
|
||||||
|
|
||||||
|
def traverse_node(node):
|
||||||
|
if node.get('type') == 'TEXT':
|
||||||
|
style = node.get('style', {})
|
||||||
|
if style:
|
||||||
|
text_styles.append({
|
||||||
|
'font_family': style.get('fontFamily', ''),
|
||||||
|
'font_size': style.get('fontSize', 0),
|
||||||
|
'font_weight': style.get('fontWeight', 400),
|
||||||
|
'line_height': style.get('lineHeightPx', 0),
|
||||||
|
'letter_spacing': style.get('letterSpacing', 0),
|
||||||
|
'node_id': node.get('id'),
|
||||||
|
'node_name': node.get('name', ''),
|
||||||
|
'text': node.get('characters', '')
|
||||||
|
})
|
||||||
|
|
||||||
|
for child in node.get('children', []):
|
||||||
|
traverse_node(child)
|
||||||
|
|
||||||
|
if 'document' in file_data:
|
||||||
|
traverse_node(file_data['document'])
|
||||||
|
|
||||||
|
return text_styles
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""CLI interface for Figma operations"""
|
||||||
|
parser = argparse.ArgumentParser(description='Figma API Client')
|
||||||
|
parser.add_argument('command', choices=[
|
||||||
|
'get-file', 'export-images', 'get-components', 'get-styles',
|
||||||
|
'extract-colors', 'extract-typography', 'user-info'
|
||||||
|
])
|
||||||
|
parser.add_argument('file_key', nargs='?', help='Figma file key or URL')
|
||||||
|
parser.add_argument('--node-ids', help='Comma-separated node IDs')
|
||||||
|
parser.add_argument('--format', default='png', choices=['png', 'svg', 'pdf'])
|
||||||
|
parser.add_argument('--scale', type=float, default=1.0)
|
||||||
|
parser.add_argument('--output', help='Output file path')
|
||||||
|
parser.add_argument('--token', help='Figma access token (overrides env var)')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = FigmaClient(access_token=args.token)
|
||||||
|
|
||||||
|
if args.command == 'get-file':
|
||||||
|
if not args.file_key:
|
||||||
|
parser.error('file_key required for get-file command')
|
||||||
|
|
||||||
|
file_key = client.parse_file_url(args.file_key)
|
||||||
|
result = client.get_file(file_key)
|
||||||
|
|
||||||
|
elif args.command == 'export-images':
|
||||||
|
if not args.file_key or not args.node_ids:
|
||||||
|
parser.error('file_key and --node-ids required for export-images command')
|
||||||
|
|
||||||
|
file_key = client.parse_file_url(args.file_key)
|
||||||
|
result = client.export_images(
|
||||||
|
file_key,
|
||||||
|
args.node_ids.split(','),
|
||||||
|
format=args.format,
|
||||||
|
scale=args.scale
|
||||||
|
)
|
||||||
|
|
||||||
|
elif args.command == 'get-components':
|
||||||
|
if not args.file_key:
|
||||||
|
parser.error('file_key required for get-components command')
|
||||||
|
|
||||||
|
file_key = client.parse_file_url(args.file_key)
|
||||||
|
result = client.get_file_components(file_key)
|
||||||
|
|
||||||
|
elif args.command == 'get-styles':
|
||||||
|
if not args.file_key:
|
||||||
|
parser.error('file_key required for get-styles command')
|
||||||
|
|
||||||
|
file_key = client.parse_file_url(args.file_key)
|
||||||
|
result = client.get_file_styles(file_key)
|
||||||
|
|
||||||
|
elif args.command == 'extract-colors':
|
||||||
|
if not args.file_key:
|
||||||
|
parser.error('file_key required for extract-colors command')
|
||||||
|
|
||||||
|
file_key = client.parse_file_url(args.file_key)
|
||||||
|
file_data = client.get_file(file_key)
|
||||||
|
result = client.extract_colors(file_data)
|
||||||
|
|
||||||
|
elif args.command == 'extract-typography':
|
||||||
|
if not args.file_key:
|
||||||
|
parser.error('file_key required for extract-typography command')
|
||||||
|
|
||||||
|
file_key = client.parse_file_url(args.file_key)
|
||||||
|
file_data = client.get_file(file_key)
|
||||||
|
result = client.extract_text_styles(file_data)
|
||||||
|
|
||||||
|
elif args.command == 'user-info':
|
||||||
|
result = client.get_user_info()
|
||||||
|
|
||||||
|
# Output result
|
||||||
|
output = json.dumps(result, indent=2)
|
||||||
|
|
||||||
|
if args.output:
|
||||||
|
with open(args.output, 'w') as f:
|
||||||
|
f.write(output)
|
||||||
|
print(f"Output saved to {args.output}")
|
||||||
|
else:
|
||||||
|
print(output)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
768
scripts/style_auditor.py
Normal file
768
scripts/style_auditor.py
Normal file
@@ -0,0 +1,768 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Figma Style Auditor - Design system analysis and consistency checking
|
||||||
|
Analyzes files for brand compliance, accessibility, and design system health.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import time
|
||||||
|
from typing import Dict, List, Optional, Union, Any, Tuple
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
import argparse
|
||||||
|
import colorsys
|
||||||
|
try:
|
||||||
|
from figma_client import FigmaClient
|
||||||
|
except ImportError:
|
||||||
|
# Handle case where script is run directly
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
sys.path.append(str(Path(__file__).parent))
|
||||||
|
from figma_client import FigmaClient
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AuditConfig:
|
||||||
|
"""Configuration for design system audits"""
|
||||||
|
check_accessibility: bool = True
|
||||||
|
check_brand_compliance: bool = True
|
||||||
|
check_consistency: bool = True
|
||||||
|
generate_report: bool = True
|
||||||
|
min_contrast_ratio: float = 4.5 # WCAG AA standard
|
||||||
|
min_touch_target: float = 44 # iOS/Material Design standard
|
||||||
|
brand_colors: List[str] = field(default_factory=list)
|
||||||
|
brand_fonts: List[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AuditIssue:
|
||||||
|
"""Represents a design issue found during audit"""
|
||||||
|
severity: str # 'error', 'warning', 'info'
|
||||||
|
category: str # 'accessibility', 'brand', 'consistency'
|
||||||
|
message: str
|
||||||
|
node_id: str = None
|
||||||
|
node_name: str = None
|
||||||
|
suggestions: List[str] = field(default_factory=list)
|
||||||
|
details: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
class StyleAuditor:
|
||||||
|
"""Comprehensive design system auditor for Figma files"""
|
||||||
|
|
||||||
|
def __init__(self, figma_client: FigmaClient, config: AuditConfig = None):
|
||||||
|
self.client = figma_client
|
||||||
|
self.config = config or AuditConfig()
|
||||||
|
self.issues: List[AuditIssue] = []
|
||||||
|
|
||||||
|
def audit_file(self, file_key: str) -> Dict[str, Any]:
|
||||||
|
"""Perform comprehensive audit of a Figma file"""
|
||||||
|
|
||||||
|
print(f"Starting audit of file: {file_key}")
|
||||||
|
self.issues.clear()
|
||||||
|
|
||||||
|
# Get file data
|
||||||
|
file_data = self.client.get_file(file_key)
|
||||||
|
|
||||||
|
# Run audit checks
|
||||||
|
if self.config.check_accessibility:
|
||||||
|
self._audit_accessibility(file_data)
|
||||||
|
|
||||||
|
if self.config.check_brand_compliance:
|
||||||
|
self._audit_brand_compliance(file_data)
|
||||||
|
|
||||||
|
if self.config.check_consistency:
|
||||||
|
self._audit_consistency(file_data)
|
||||||
|
|
||||||
|
# Generate summary
|
||||||
|
summary = self._generate_summary()
|
||||||
|
|
||||||
|
print(f"Audit completed: {len(self.issues)} issues found")
|
||||||
|
return {
|
||||||
|
'file_key': file_key,
|
||||||
|
'file_name': file_data.get('name', 'Unknown'),
|
||||||
|
'audit_timestamp': time.time(),
|
||||||
|
'summary': summary,
|
||||||
|
'issues': [self._issue_to_dict(issue) for issue in self.issues],
|
||||||
|
'recommendations': self._generate_recommendations()
|
||||||
|
}
|
||||||
|
|
||||||
|
def audit_multiple_files(self, file_keys: List[str]) -> Dict[str, Any]:
|
||||||
|
"""Audit multiple files and generate comparative analysis"""
|
||||||
|
|
||||||
|
all_results = {}
|
||||||
|
aggregated_issues = []
|
||||||
|
|
||||||
|
for file_key in file_keys:
|
||||||
|
try:
|
||||||
|
result = self.audit_file(file_key)
|
||||||
|
all_results[file_key] = result
|
||||||
|
aggregated_issues.extend(self.issues)
|
||||||
|
print(f"✓ Audited {result['file_name']}: {len(result['issues'])} issues")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Failed to audit {file_key}: {e}")
|
||||||
|
all_results[file_key] = {'error': str(e)}
|
||||||
|
|
||||||
|
# Generate cross-file analysis
|
||||||
|
cross_file_analysis = self._analyze_cross_file_patterns(all_results)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'individual_audits': all_results,
|
||||||
|
'cross_file_analysis': cross_file_analysis,
|
||||||
|
'total_files': len(file_keys),
|
||||||
|
'successful_audits': len([r for r in all_results.values() if 'error' not in r])
|
||||||
|
}
|
||||||
|
|
||||||
|
def _audit_accessibility(self, file_data: Dict[str, Any]):
|
||||||
|
"""Check accessibility compliance (WCAG guidelines)"""
|
||||||
|
|
||||||
|
def audit_node_accessibility(node):
|
||||||
|
node_type = node.get('type', '')
|
||||||
|
node_name = node.get('name', '')
|
||||||
|
node_id = node.get('id', '')
|
||||||
|
|
||||||
|
# Check text contrast
|
||||||
|
if node_type == 'TEXT':
|
||||||
|
self._check_text_contrast(node)
|
||||||
|
|
||||||
|
# Check touch targets
|
||||||
|
if node_type in ['COMPONENT', 'INSTANCE', 'FRAME'] and 'button' in node_name.lower():
|
||||||
|
self._check_touch_target_size(node)
|
||||||
|
|
||||||
|
# Check focus indicators
|
||||||
|
if 'interactive' in node_name.lower() or 'button' in node_name.lower():
|
||||||
|
self._check_focus_indicators(node)
|
||||||
|
|
||||||
|
# Recursively check children
|
||||||
|
for child in node.get('children', []):
|
||||||
|
audit_node_accessibility(child)
|
||||||
|
|
||||||
|
if 'document' in file_data:
|
||||||
|
audit_node_accessibility(file_data['document'])
|
||||||
|
|
||||||
|
def _audit_brand_compliance(self, file_data: Dict[str, Any]):
|
||||||
|
"""Check compliance with brand guidelines"""
|
||||||
|
|
||||||
|
if not self.config.brand_colors and not self.config.brand_fonts:
|
||||||
|
return # Skip if no brand guidelines configured
|
||||||
|
|
||||||
|
def audit_node_brand(node):
|
||||||
|
# Check color compliance
|
||||||
|
if 'fills' in node:
|
||||||
|
self._check_brand_colors(node)
|
||||||
|
|
||||||
|
# Check font compliance
|
||||||
|
if node.get('type') == 'TEXT':
|
||||||
|
self._check_brand_fonts(node)
|
||||||
|
|
||||||
|
# Recursively check children
|
||||||
|
for child in node.get('children', []):
|
||||||
|
audit_node_brand(child)
|
||||||
|
|
||||||
|
if 'document' in file_data:
|
||||||
|
audit_node_brand(file_data['document'])
|
||||||
|
|
||||||
|
def _audit_consistency(self, file_data: Dict[str, Any]):
|
||||||
|
"""Check internal consistency within the file"""
|
||||||
|
|
||||||
|
# Collect all styles for analysis
|
||||||
|
colors_used = []
|
||||||
|
fonts_used = []
|
||||||
|
spacing_used = []
|
||||||
|
|
||||||
|
def collect_styles(node):
|
||||||
|
# Collect colors
|
||||||
|
if 'fills' in node:
|
||||||
|
for fill in node.get('fills', []):
|
||||||
|
if fill.get('type') == 'SOLID':
|
||||||
|
color = fill.get('color', {})
|
||||||
|
if color:
|
||||||
|
colors_used.append({
|
||||||
|
'color': color,
|
||||||
|
'node_id': node.get('id'),
|
||||||
|
'node_name': node.get('name', '')
|
||||||
|
})
|
||||||
|
|
||||||
|
# Collect fonts
|
||||||
|
if node.get('type') == 'TEXT':
|
||||||
|
style = node.get('style', {})
|
||||||
|
if style:
|
||||||
|
fonts_used.append({
|
||||||
|
'font_family': style.get('fontFamily', ''),
|
||||||
|
'font_size': style.get('fontSize', 0),
|
||||||
|
'font_weight': style.get('fontWeight', 400),
|
||||||
|
'node_id': node.get('id'),
|
||||||
|
'node_name': node.get('name', '')
|
||||||
|
})
|
||||||
|
|
||||||
|
# Collect spacing (approximation from layout)
|
||||||
|
if 'children' in node and len(node['children']) > 1:
|
||||||
|
# This would need more sophisticated analysis
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Recursively collect from children
|
||||||
|
for child in node.get('children', []):
|
||||||
|
collect_styles(child)
|
||||||
|
|
||||||
|
if 'document' in file_data:
|
||||||
|
collect_styles(file_data['document'])
|
||||||
|
|
||||||
|
# Analyze collected styles
|
||||||
|
self._analyze_color_consistency(colors_used)
|
||||||
|
self._analyze_typography_consistency(fonts_used)
|
||||||
|
|
||||||
|
def _check_text_contrast(self, text_node: Dict[str, Any]):
|
||||||
|
"""Check if text has sufficient contrast against background"""
|
||||||
|
|
||||||
|
# This is a simplified implementation
|
||||||
|
# Real implementation would need to calculate actual contrast
|
||||||
|
fills = text_node.get('fills', [])
|
||||||
|
if not fills:
|
||||||
|
return
|
||||||
|
|
||||||
|
text_color = fills[0].get('color', {})
|
||||||
|
if not text_color:
|
||||||
|
return
|
||||||
|
|
||||||
|
# For now, assume white background (would need parent background detection)
|
||||||
|
bg_color = {'r': 1, 'g': 1, 'b': 1} # White
|
||||||
|
|
||||||
|
contrast_ratio = self._calculate_contrast_ratio(text_color, bg_color)
|
||||||
|
|
||||||
|
if contrast_ratio < self.config.min_contrast_ratio:
|
||||||
|
self.issues.append(AuditIssue(
|
||||||
|
severity='error',
|
||||||
|
category='accessibility',
|
||||||
|
message=f'Insufficient color contrast: {contrast_ratio:.1f}:1 (minimum: {self.config.min_contrast_ratio}:1)',
|
||||||
|
node_id=text_node.get('id'),
|
||||||
|
node_name=text_node.get('name', ''),
|
||||||
|
suggestions=[
|
||||||
|
'Darken text color or lighten background',
|
||||||
|
'Use high contrast color combinations',
|
||||||
|
'Test with accessibility tools'
|
||||||
|
],
|
||||||
|
details={'contrast_ratio': contrast_ratio, 'text_color': text_color}
|
||||||
|
))
|
||||||
|
|
||||||
|
def _check_touch_target_size(self, node: Dict[str, Any]):
|
||||||
|
"""Check if interactive elements meet minimum touch target size"""
|
||||||
|
|
||||||
|
bounds = node.get('absoluteBoundingBox', {})
|
||||||
|
if not bounds:
|
||||||
|
return
|
||||||
|
|
||||||
|
width = bounds.get('width', 0)
|
||||||
|
height = bounds.get('height', 0)
|
||||||
|
|
||||||
|
if width < self.config.min_touch_target or height < self.config.min_touch_target:
|
||||||
|
self.issues.append(AuditIssue(
|
||||||
|
severity='warning',
|
||||||
|
category='accessibility',
|
||||||
|
message=f'Touch target too small: {width}×{height}px (minimum: {self.config.min_touch_target}×{self.config.min_touch_target}px)',
|
||||||
|
node_id=node.get('id'),
|
||||||
|
node_name=node.get('name', ''),
|
||||||
|
suggestions=[
|
||||||
|
f'Increase size to at least {self.config.min_touch_target}×{self.config.min_touch_target}px',
|
||||||
|
'Add padding around interactive elements',
|
||||||
|
'Consider user interaction patterns'
|
||||||
|
],
|
||||||
|
details={'current_size': {'width': width, 'height': height}}
|
||||||
|
))
|
||||||
|
|
||||||
|
def _check_focus_indicators(self, node: Dict[str, Any]):
|
||||||
|
"""Check if interactive elements have proper focus indicators"""
|
||||||
|
|
||||||
|
# This would check for focus states, outlines, etc.
|
||||||
|
# For now, just flag interactive elements that might need focus indicators
|
||||||
|
|
||||||
|
effects = node.get('effects', [])
|
||||||
|
has_focus_effect = any(
|
||||||
|
effect.get('type') == 'DROP_SHADOW' and
|
||||||
|
'focus' in str(effect).lower()
|
||||||
|
for effect in effects
|
||||||
|
)
|
||||||
|
|
||||||
|
if not has_focus_effect:
|
||||||
|
self.issues.append(AuditIssue(
|
||||||
|
severity='info',
|
||||||
|
category='accessibility',
|
||||||
|
message='Interactive element may need focus indicator',
|
||||||
|
node_id=node.get('id'),
|
||||||
|
node_name=node.get('name', ''),
|
||||||
|
suggestions=[
|
||||||
|
'Add focus state with visible outline',
|
||||||
|
'Use consistent focus indicator style',
|
||||||
|
'Test keyboard navigation'
|
||||||
|
]
|
||||||
|
))
|
||||||
|
|
||||||
|
def _check_brand_colors(self, node: Dict[str, Any]):
|
||||||
|
"""Check if colors match brand guidelines"""
|
||||||
|
|
||||||
|
if not self.config.brand_colors:
|
||||||
|
return
|
||||||
|
|
||||||
|
fills = node.get('fills', [])
|
||||||
|
for fill in fills:
|
||||||
|
if fill.get('type') == 'SOLID':
|
||||||
|
color = fill.get('color', {})
|
||||||
|
if color:
|
||||||
|
hex_color = self._rgb_to_hex(color)
|
||||||
|
|
||||||
|
if hex_color not in self.config.brand_colors:
|
||||||
|
# Check if it's close to a brand color
|
||||||
|
closest_brand_color = self._find_closest_brand_color(hex_color)
|
||||||
|
|
||||||
|
self.issues.append(AuditIssue(
|
||||||
|
severity='warning',
|
||||||
|
category='brand',
|
||||||
|
message=f'Non-brand color used: {hex_color}',
|
||||||
|
node_id=node.get('id'),
|
||||||
|
node_name=node.get('name', ''),
|
||||||
|
suggestions=[
|
||||||
|
f'Consider using brand color: {closest_brand_color}',
|
||||||
|
'Check brand color palette',
|
||||||
|
'Use design system colors'
|
||||||
|
],
|
||||||
|
details={'used_color': hex_color, 'suggested_color': closest_brand_color}
|
||||||
|
))
|
||||||
|
|
||||||
|
def _check_brand_fonts(self, text_node: Dict[str, Any]):
|
||||||
|
"""Check if fonts match brand guidelines"""
|
||||||
|
|
||||||
|
if not self.config.brand_fonts:
|
||||||
|
return
|
||||||
|
|
||||||
|
style = text_node.get('style', {})
|
||||||
|
font_family = style.get('fontFamily', '')
|
||||||
|
|
||||||
|
if font_family and font_family not in self.config.brand_fonts:
|
||||||
|
self.issues.append(AuditIssue(
|
||||||
|
severity='warning',
|
||||||
|
category='brand',
|
||||||
|
message=f'Non-brand font used: {font_family}',
|
||||||
|
node_id=text_node.get('id'),
|
||||||
|
node_name=text_node.get('name', ''),
|
||||||
|
suggestions=[
|
||||||
|
f'Use brand fonts: {", ".join(self.config.brand_fonts)}',
|
||||||
|
'Check typography guidelines',
|
||||||
|
'Maintain font consistency'
|
||||||
|
],
|
||||||
|
details={'used_font': font_family, 'brand_fonts': self.config.brand_fonts}
|
||||||
|
))
|
||||||
|
|
||||||
|
def _analyze_color_consistency(self, colors_used: List[Dict[str, Any]]):
|
||||||
|
"""Analyze color usage patterns for consistency issues"""
|
||||||
|
|
||||||
|
# Group similar colors
|
||||||
|
color_groups = {}
|
||||||
|
|
||||||
|
for color_data in colors_used:
|
||||||
|
color = color_data['color']
|
||||||
|
hex_color = self._rgb_to_hex(color)
|
||||||
|
|
||||||
|
# Find similar colors (within tolerance)
|
||||||
|
similar_group = None
|
||||||
|
for group_color in color_groups.keys():
|
||||||
|
if self._colors_are_similar(hex_color, group_color):
|
||||||
|
similar_group = group_color
|
||||||
|
break
|
||||||
|
|
||||||
|
if similar_group:
|
||||||
|
color_groups[similar_group].append(color_data)
|
||||||
|
else:
|
||||||
|
color_groups[hex_color] = [color_data]
|
||||||
|
|
||||||
|
# Flag groups with multiple similar but not identical colors
|
||||||
|
for base_color, group in color_groups.items():
|
||||||
|
if len(group) > 1:
|
||||||
|
unique_colors = set(self._rgb_to_hex(item['color']) for item in group)
|
||||||
|
if len(unique_colors) > 1:
|
||||||
|
self.issues.append(AuditIssue(
|
||||||
|
severity='info',
|
||||||
|
category='consistency',
|
||||||
|
message=f'Multiple similar colors found: {", ".join(unique_colors)}',
|
||||||
|
suggestions=[
|
||||||
|
'Standardize similar colors',
|
||||||
|
'Use design system color tokens',
|
||||||
|
'Review color palette'
|
||||||
|
],
|
||||||
|
details={'similar_colors': list(unique_colors), 'usage_count': len(group)}
|
||||||
|
))
|
||||||
|
|
||||||
|
def _analyze_typography_consistency(self, fonts_used: List[Dict[str, Any]]):
|
||||||
|
"""Analyze typography usage for consistency"""
|
||||||
|
|
||||||
|
# Group by font family and size
|
||||||
|
font_combinations = {}
|
||||||
|
|
||||||
|
for font_data in fonts_used:
|
||||||
|
key = f"{font_data['font_family']}-{font_data['font_size']}pt-{font_data['font_weight']}"
|
||||||
|
if key not in font_combinations:
|
||||||
|
font_combinations[key] = []
|
||||||
|
font_combinations[key].append(font_data)
|
||||||
|
|
||||||
|
# Look for too many font variations
|
||||||
|
families = set(font['font_family'] for font in fonts_used)
|
||||||
|
sizes = set(font['font_size'] for font in fonts_used)
|
||||||
|
|
||||||
|
if len(families) > 3:
|
||||||
|
self.issues.append(AuditIssue(
|
||||||
|
severity='warning',
|
||||||
|
category='consistency',
|
||||||
|
message=f'Too many font families: {len(families)} ({", ".join(families)})',
|
||||||
|
suggestions=[
|
||||||
|
'Reduce to 2-3 font families maximum',
|
||||||
|
'Establish typography hierarchy',
|
||||||
|
'Use consistent font pairing'
|
||||||
|
]
|
||||||
|
))
|
||||||
|
|
||||||
|
if len(sizes) > 8:
|
||||||
|
self.issues.append(AuditIssue(
|
||||||
|
severity='info',
|
||||||
|
category='consistency',
|
||||||
|
message=f'Many font sizes used: {len(sizes)} different sizes',
|
||||||
|
suggestions=[
|
||||||
|
'Create modular typography scale',
|
||||||
|
'Reduce to 6-8 standard sizes',
|
||||||
|
'Use consistent size progression'
|
||||||
|
]
|
||||||
|
))
|
||||||
|
|
||||||
|
def _calculate_contrast_ratio(self, color1: Dict[str, float], color2: Dict[str, float]) -> float:
|
||||||
|
"""Calculate WCAG contrast ratio between two colors"""
|
||||||
|
|
||||||
|
def get_luminance(color):
|
||||||
|
"""Calculate relative luminance"""
|
||||||
|
def linearize(val):
|
||||||
|
if val <= 0.03928:
|
||||||
|
return val / 12.92
|
||||||
|
else:
|
||||||
|
return pow((val + 0.055) / 1.055, 2.4)
|
||||||
|
|
||||||
|
r = linearize(color.get('r', 0))
|
||||||
|
g = linearize(color.get('g', 0))
|
||||||
|
b = linearize(color.get('b', 0))
|
||||||
|
|
||||||
|
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
||||||
|
|
||||||
|
lum1 = get_luminance(color1)
|
||||||
|
lum2 = get_luminance(color2)
|
||||||
|
|
||||||
|
lighter = max(lum1, lum2)
|
||||||
|
darker = min(lum1, lum2)
|
||||||
|
|
||||||
|
return (lighter + 0.05) / (darker + 0.05)
|
||||||
|
|
||||||
|
def _rgb_to_hex(self, color: Dict[str, float]) -> str:
|
||||||
|
"""Convert RGB color to hex string"""
|
||||||
|
r = int(color.get('r', 0) * 255)
|
||||||
|
g = int(color.get('g', 0) * 255)
|
||||||
|
b = int(color.get('b', 0) * 255)
|
||||||
|
return f"#{r:02x}{g:02x}{b:02x}"
|
||||||
|
|
||||||
|
def _colors_are_similar(self, color1: str, color2: str, tolerance: int = 30) -> bool:
|
||||||
|
"""Check if two hex colors are similar within tolerance"""
|
||||||
|
|
||||||
|
def hex_to_rgb(hex_color):
|
||||||
|
hex_color = hex_color.lstrip('#')
|
||||||
|
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
||||||
|
|
||||||
|
rgb1 = hex_to_rgb(color1)
|
||||||
|
rgb2 = hex_to_rgb(color2)
|
||||||
|
|
||||||
|
distance = math.sqrt(sum((a - b) ** 2 for a, b in zip(rgb1, rgb2)))
|
||||||
|
return distance < tolerance
|
||||||
|
|
||||||
|
def _find_closest_brand_color(self, hex_color: str) -> str:
|
||||||
|
"""Find the closest brand color to the given color"""
|
||||||
|
|
||||||
|
if not self.config.brand_colors:
|
||||||
|
return hex_color
|
||||||
|
|
||||||
|
min_distance = float('inf')
|
||||||
|
closest_color = self.config.brand_colors[0]
|
||||||
|
|
||||||
|
for brand_color in self.config.brand_colors:
|
||||||
|
if self._colors_are_similar(hex_color, brand_color, tolerance=255): # Use max tolerance for distance calc
|
||||||
|
distance = self._color_distance(hex_color, brand_color)
|
||||||
|
if distance < min_distance:
|
||||||
|
min_distance = distance
|
||||||
|
closest_color = brand_color
|
||||||
|
|
||||||
|
return closest_color
|
||||||
|
|
||||||
|
def _color_distance(self, color1: str, color2: str) -> float:
|
||||||
|
"""Calculate Euclidean distance between two colors"""
|
||||||
|
|
||||||
|
def hex_to_rgb(hex_color):
|
||||||
|
hex_color = hex_color.lstrip('#')
|
||||||
|
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
||||||
|
|
||||||
|
rgb1 = hex_to_rgb(color1)
|
||||||
|
rgb2 = hex_to_rgb(color2)
|
||||||
|
|
||||||
|
return math.sqrt(sum((a - b) ** 2 for a, b in zip(rgb1, rgb2)))
|
||||||
|
|
||||||
|
def _generate_summary(self) -> Dict[str, Any]:
|
||||||
|
"""Generate audit summary statistics"""
|
||||||
|
|
||||||
|
summary = {
|
||||||
|
'total_issues': len(self.issues),
|
||||||
|
'by_severity': {
|
||||||
|
'error': len([i for i in self.issues if i.severity == 'error']),
|
||||||
|
'warning': len([i for i in self.issues if i.severity == 'warning']),
|
||||||
|
'info': len([i for i in self.issues if i.severity == 'info'])
|
||||||
|
},
|
||||||
|
'by_category': {
|
||||||
|
'accessibility': len([i for i in self.issues if i.category == 'accessibility']),
|
||||||
|
'brand': len([i for i in self.issues if i.category == 'brand']),
|
||||||
|
'consistency': len([i for i in self.issues if i.category == 'consistency'])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Calculate overall score (0-100)
|
||||||
|
max_score = 100
|
||||||
|
error_penalty = 10
|
||||||
|
warning_penalty = 3
|
||||||
|
info_penalty = 1
|
||||||
|
|
||||||
|
penalty = (summary['by_severity']['error'] * error_penalty +
|
||||||
|
summary['by_severity']['warning'] * warning_penalty +
|
||||||
|
summary['by_severity']['info'] * info_penalty)
|
||||||
|
|
||||||
|
summary['score'] = max(0, max_score - penalty)
|
||||||
|
summary['grade'] = self._score_to_grade(summary['score'])
|
||||||
|
|
||||||
|
return summary
|
||||||
|
|
||||||
|
def _score_to_grade(self, score: int) -> str:
|
||||||
|
"""Convert numeric score to letter grade"""
|
||||||
|
if score >= 90:
|
||||||
|
return 'A'
|
||||||
|
elif score >= 80:
|
||||||
|
return 'B'
|
||||||
|
elif score >= 70:
|
||||||
|
return 'C'
|
||||||
|
elif score >= 60:
|
||||||
|
return 'D'
|
||||||
|
else:
|
||||||
|
return 'F'
|
||||||
|
|
||||||
|
def _generate_recommendations(self) -> List[str]:
|
||||||
|
"""Generate overall recommendations based on audit results"""
|
||||||
|
|
||||||
|
recommendations = []
|
||||||
|
|
||||||
|
error_count = len([i for i in self.issues if i.severity == 'error'])
|
||||||
|
warning_count = len([i for i in self.issues if i.severity == 'warning'])
|
||||||
|
|
||||||
|
if error_count > 0:
|
||||||
|
recommendations.append(f"Fix {error_count} critical accessibility issues immediately")
|
||||||
|
|
||||||
|
if warning_count > 5:
|
||||||
|
recommendations.append("Review and address design consistency issues")
|
||||||
|
|
||||||
|
brand_issues = len([i for i in self.issues if i.category == 'brand'])
|
||||||
|
if brand_issues > 0:
|
||||||
|
recommendations.append("Establish and enforce brand guidelines")
|
||||||
|
|
||||||
|
consistency_issues = len([i for i in self.issues if i.category == 'consistency'])
|
||||||
|
if consistency_issues > 3:
|
||||||
|
recommendations.append("Create and apply design system standards")
|
||||||
|
|
||||||
|
if not recommendations:
|
||||||
|
recommendations.append("Great work! Consider periodic design system reviews")
|
||||||
|
|
||||||
|
return recommendations
|
||||||
|
|
||||||
|
def _analyze_cross_file_patterns(self, all_results: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Analyze patterns across multiple files"""
|
||||||
|
|
||||||
|
# This would analyze common issues across files
|
||||||
|
# For now, return basic aggregation
|
||||||
|
|
||||||
|
total_issues = 0
|
||||||
|
common_issues = {}
|
||||||
|
|
||||||
|
for file_key, result in all_results.items():
|
||||||
|
if 'error' not in result:
|
||||||
|
total_issues += result['summary']['total_issues']
|
||||||
|
|
||||||
|
for issue in result['issues']:
|
||||||
|
issue_type = f"{issue['category']}:{issue['message'].split(':')[0]}"
|
||||||
|
common_issues[issue_type] = common_issues.get(issue_type, 0) + 1
|
||||||
|
|
||||||
|
# Find most common issues
|
||||||
|
most_common = sorted(common_issues.items(), key=lambda x: x[1], reverse=True)[:5]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'total_issues_across_files': total_issues,
|
||||||
|
'most_common_issues': most_common,
|
||||||
|
'files_with_errors': len([r for r in all_results.values() if 'error' not in r and r['summary']['by_severity']['error'] > 0])
|
||||||
|
}
|
||||||
|
|
||||||
|
def _issue_to_dict(self, issue: AuditIssue) -> Dict[str, Any]:
|
||||||
|
"""Convert AuditIssue to dictionary for JSON serialization"""
|
||||||
|
return {
|
||||||
|
'severity': issue.severity,
|
||||||
|
'category': issue.category,
|
||||||
|
'message': issue.message,
|
||||||
|
'node_id': issue.node_id,
|
||||||
|
'node_name': issue.node_name,
|
||||||
|
'suggestions': issue.suggestions,
|
||||||
|
'details': issue.details
|
||||||
|
}
|
||||||
|
|
||||||
|
def generate_report(self, audit_results: Dict[str, Any], output_path: str = None) -> str:
|
||||||
|
"""Generate comprehensive audit report"""
|
||||||
|
|
||||||
|
if not output_path:
|
||||||
|
output_path = 'figma-audit-report.html'
|
||||||
|
|
||||||
|
html_report = self._create_html_report(audit_results)
|
||||||
|
|
||||||
|
with open(output_path, 'w') as f:
|
||||||
|
f.write(html_report)
|
||||||
|
|
||||||
|
print(f"Audit report generated: {output_path}")
|
||||||
|
return output_path
|
||||||
|
|
||||||
|
def _create_html_report(self, audit_results: Dict[str, Any]) -> str:
|
||||||
|
"""Create HTML audit report"""
|
||||||
|
|
||||||
|
# This would generate a comprehensive HTML report
|
||||||
|
# For now, return basic HTML structure
|
||||||
|
|
||||||
|
html = f"""<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Figma Design Audit Report</title>
|
||||||
|
<style>
|
||||||
|
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; margin: 40px; }}
|
||||||
|
.header {{ border-bottom: 2px solid #007AFF; padding-bottom: 20px; margin-bottom: 30px; }}
|
||||||
|
.summary {{ background: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 30px; }}
|
||||||
|
.issue {{ margin-bottom: 20px; padding: 15px; border-left: 4px solid #ddd; }}
|
||||||
|
.error {{ border-color: #dc3545; background: #f8d7da; }}
|
||||||
|
.warning {{ border-color: #ffc107; background: #fff3cd; }}
|
||||||
|
.info {{ border-color: #17a2b8; background: #d1ecf1; }}
|
||||||
|
.grade {{ font-size: 48px; font-weight: bold; color: #007AFF; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1>Figma Design Audit Report</h1>
|
||||||
|
<p>File: {audit_results.get('file_name', 'Unknown')}</p>
|
||||||
|
<p>Audit Date: {time.strftime('%Y-%m-%d %H:%M:%S')}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="summary">
|
||||||
|
<h2>Summary</h2>
|
||||||
|
<div style="display: flex; align-items: center; gap: 40px;">
|
||||||
|
<div>
|
||||||
|
<div class="grade">{audit_results['summary']['grade']}</div>
|
||||||
|
<div>Score: {audit_results['summary']['score']}/100</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p><strong>Total Issues:</strong> {audit_results['summary']['total_issues']}</p>
|
||||||
|
<p><strong>Errors:</strong> {audit_results['summary']['by_severity']['error']}</p>
|
||||||
|
<p><strong>Warnings:</strong> {audit_results['summary']['by_severity']['warning']}</p>
|
||||||
|
<p><strong>Info:</strong> {audit_results['summary']['by_severity']['info']}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Issues Found</h2>
|
||||||
|
"""
|
||||||
|
|
||||||
|
for issue in audit_results['issues']:
|
||||||
|
severity_class = issue['severity']
|
||||||
|
html += f"""
|
||||||
|
<div class="issue {severity_class}">
|
||||||
|
<h3>{issue['category'].title()}: {issue['message']}</h3>
|
||||||
|
<p><strong>Node:</strong> {issue.get('node_name', 'N/A')} ({issue.get('node_id', 'N/A')})</p>
|
||||||
|
<p><strong>Suggestions:</strong></p>
|
||||||
|
<ul>
|
||||||
|
"""
|
||||||
|
for suggestion in issue.get('suggestions', []):
|
||||||
|
html += f"<li>{suggestion}</li>\n"
|
||||||
|
|
||||||
|
html += "</ul></div>\n"
|
||||||
|
|
||||||
|
html += """
|
||||||
|
<h2>Recommendations</h2>
|
||||||
|
<ul>
|
||||||
|
"""
|
||||||
|
|
||||||
|
for rec in audit_results['recommendations']:
|
||||||
|
html += f"<li>{rec}</li>\n"
|
||||||
|
|
||||||
|
html += """
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
return html
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""CLI interface for style auditing"""
|
||||||
|
parser = argparse.ArgumentParser(description='Figma Style Auditor')
|
||||||
|
parser.add_argument('command', choices=['audit-file', 'audit-multiple', 'audit-brand'])
|
||||||
|
parser.add_argument('file_keys', help='File key(s) or path to file list')
|
||||||
|
parser.add_argument('--output', help='Output file for audit report')
|
||||||
|
parser.add_argument('--brand-colors', help='Comma-separated list of brand hex colors')
|
||||||
|
parser.add_argument('--brand-fonts', help='Comma-separated list of brand fonts')
|
||||||
|
parser.add_argument('--min-contrast', type=float, default=4.5, help='Minimum contrast ratio')
|
||||||
|
parser.add_argument('--generate-html', action='store_true', help='Generate HTML report')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = FigmaClient()
|
||||||
|
|
||||||
|
# Configure auditor
|
||||||
|
config = AuditConfig(
|
||||||
|
min_contrast_ratio=args.min_contrast,
|
||||||
|
brand_colors=args.brand_colors.split(',') if args.brand_colors else [],
|
||||||
|
brand_fonts=args.brand_fonts.split(',') if args.brand_fonts else [],
|
||||||
|
generate_report=args.generate_html
|
||||||
|
)
|
||||||
|
|
||||||
|
auditor = StyleAuditor(client, config)
|
||||||
|
|
||||||
|
if args.command == 'audit-file':
|
||||||
|
file_key = client.parse_file_url(args.file_keys)
|
||||||
|
result = auditor.audit_file(file_key)
|
||||||
|
|
||||||
|
elif args.command == 'audit-multiple':
|
||||||
|
# Parse file keys (could be comma-separated or from file)
|
||||||
|
if os.path.isfile(args.file_keys):
|
||||||
|
with open(args.file_keys) as f:
|
||||||
|
file_keys = [line.strip() for line in f if line.strip()]
|
||||||
|
else:
|
||||||
|
file_keys = args.file_keys.split(',')
|
||||||
|
|
||||||
|
file_keys = [client.parse_file_url(key) for key in file_keys]
|
||||||
|
result = auditor.audit_multiple_files(file_keys)
|
||||||
|
|
||||||
|
# Output results
|
||||||
|
output_content = json.dumps(result, indent=2)
|
||||||
|
|
||||||
|
if args.output:
|
||||||
|
with open(args.output, 'w') as f:
|
||||||
|
f.write(output_content)
|
||||||
|
print(f"Audit results saved to {args.output}")
|
||||||
|
else:
|
||||||
|
print(output_content)
|
||||||
|
|
||||||
|
# Generate HTML report if requested
|
||||||
|
if args.generate_html and args.command == 'audit-file':
|
||||||
|
html_path = args.output.replace('.json', '.html') if args.output else 'audit-report.html'
|
||||||
|
auditor.generate_report(result, html_path)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user