commit 8efab1561a90cf84ec3247c461e58ee56457e87f Author: zlei9 Date: Sun Mar 29 13:02:21 2026 +0800 Initial commit with translated description diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..19ca704 --- /dev/null +++ b/SKILL.md @@ -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. \ No newline at end of file diff --git a/_meta.json b/_meta.json new file mode 100644 index 0000000..4af74a0 --- /dev/null +++ b/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn74kjx2pnh0qkmk5mrsxxrtjh7zw4g8", + "slug": "figma", + "version": "2.1.0", + "publishedAt": 1769380900543 +} \ No newline at end of file diff --git a/assets/templates/brand-guidelines-template.json b/assets/templates/brand-guidelines-template.json new file mode 100644 index 0000000..f6bde44 --- /dev/null +++ b/assets/templates/brand-guidelines-template.json @@ -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" + } +} \ No newline at end of file diff --git a/references/accessibility-guidelines.md b/references/accessibility-guidelines.md new file mode 100644 index 0000000..834e9ca --- /dev/null +++ b/references/accessibility-guidelines.md @@ -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 \ No newline at end of file diff --git a/references/design-patterns.md b/references/design-patterns.md new file mode 100644 index 0000000..db839db --- /dev/null +++ b/references/design-patterns.md @@ -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 \ No newline at end of file diff --git a/references/export-formats.md b/references/export-formats.md new file mode 100644 index 0000000..8877e01 --- /dev/null +++ b/references/export-formats.md @@ -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 \ No newline at end of file diff --git a/references/figma-api-reference.md b/references/figma-api-reference.md new file mode 100644 index 0000000..7a56ace --- /dev/null +++ b/references/figma-api-reference.md @@ -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 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c3ebf5a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +requests>=2.31.0 +aiohttp>=3.9.0 +pathlib \ No newline at end of file diff --git a/scripts/accessibility_checker.py b/scripts/accessibility_checker.py new file mode 100644 index 0000000..d83ba04 --- /dev/null +++ b/scripts/accessibility_checker.py @@ -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""" + + + + + Accessibility Report - {results['file_name']} + + + +
+

🔍 Accessibility Report

+

File: {results['file_name']}

+

WCAG Level: {results['wcag_level']}

+

Generated: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(results['timestamp']))}

+
+ WCAG {results['summary']['compliance_level']} Compliance +
+
+ +
+

📊 Summary

+
+
+
{results['summary']['score']}
+
Score
+
+
+
{results['summary']['total_issues']}
+
Total Issues
+
+
+
{results['summary']['issues_by_severity']['error']}
+
Errors
+
+
+
{results['summary']['issues_by_severity']['warning']}
+
Warnings
+
+
+
+""" + + if results['issues']: + html += "

🐛 Issues Found

\n" + + for issue in results['issues']: + severity_class = issue['severity'] + html += f""" +
+

{issue['type'].replace('_', ' ').title()}: {issue['message']}

+ WCAG {issue['wcag_criterion']} +
+ Element: {issue.get('node_name', 'N/A')} + (ID: {issue.get('node_id', 'N/A')}) +
+""" + + if 'details' in issue and issue['details']: + html += "
Details:
    " + for key, value in issue['details'].items(): + html += f"
  • {key.replace('_', ' ').title()}: {value}
  • " + html += "
" + + html += "
\n" + else: + html += """ +
+

🎉 Excellent Work!

+

No accessibility issues found in this design. This indicates strong adherence to WCAG guidelines.

+
+""" + + html += """ +
+

💡 Recommendations

+ +
+ +
+

Generated by Figma Accessibility Checker | Learn more about WCAG at WCAG Quick Reference

+
+ +""" + + 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() \ No newline at end of file diff --git a/scripts/export_manager.py b/scripts/export_manager.py new file mode 100644 index 0000000..5b194c2 --- /dev/null +++ b/scripts/export_manager.py @@ -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 = () => ( +
+ Content +
+); +``` + +## 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() \ No newline at end of file diff --git a/scripts/figma_client.py b/scripts/figma_client.py new file mode 100644 index 0000000..2855698 --- /dev/null +++ b/scripts/figma_client.py @@ -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() \ No newline at end of file diff --git a/scripts/style_auditor.py b/scripts/style_auditor.py new file mode 100644 index 0000000..7ff362b --- /dev/null +++ b/scripts/style_auditor.py @@ -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""" + + + Figma Design Audit Report + + + +
+

Figma Design Audit Report

+

File: {audit_results.get('file_name', 'Unknown')}

+

Audit Date: {time.strftime('%Y-%m-%d %H:%M:%S')}

+
+ +
+

Summary

+
+
+
{audit_results['summary']['grade']}
+
Score: {audit_results['summary']['score']}/100
+
+
+

Total Issues: {audit_results['summary']['total_issues']}

+

Errors: {audit_results['summary']['by_severity']['error']}

+

Warnings: {audit_results['summary']['by_severity']['warning']}

+

Info: {audit_results['summary']['by_severity']['info']}

+
+
+
+ +

Issues Found

+""" + + for issue in audit_results['issues']: + severity_class = issue['severity'] + html += f""" +
+

{issue['category'].title()}: {issue['message']}

+

Node: {issue.get('node_name', 'N/A')} ({issue.get('node_id', 'N/A')})

+

Suggestions:

+
\n" + + html += """ +

Recommendations

+ + +""" + + 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() \ No newline at end of file