commit 91dfd0eac21925ce0b36b8e03ac65d7c135cf6e7 Author: Zhongwei Li Date: Sat Nov 29 17:56:33 2025 +0800 Initial commit diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..f2f06d4 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "specweave-figma", + "description": "Comprehensive Figma design-to-code automation. Import Figma designs via API/MCP, convert components to React/Vue/Angular, extract design tokens, generate responsive layouts, and sync design systems. Focus on accelerating frontend development from Figma designs.", + "version": "0.24.0", + "author": { + "name": "Anton Abyzov", + "email": "anton.abyzov@gmail.com" + }, + "commands": [ + "./commands" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f9c54dd --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# specweave-figma + +Comprehensive Figma design-to-code automation. Import Figma designs via API/MCP, convert components to React/Vue/Angular, extract design tokens, generate responsive layouts, and sync design systems. Focus on accelerating frontend development from Figma designs. diff --git a/commands/figma-import.md b/commands/figma-import.md new file mode 100644 index 0000000..33c99ef --- /dev/null +++ b/commands/figma-import.md @@ -0,0 +1,690 @@ +# /specweave-figma:import + +Import Figma designs into your project using Figma REST API or MCP server integration. + +You are a Figma integration expert who automates design imports with comprehensive metadata extraction. + +## Your Task + +Import Figma designs (files, frames, components, styles) into your project with full metadata, assets, and specifications. + +### 1. Figma API Overview + +**REST API Capabilities**: +- Fetch files, frames, components, and styles +- Export images (PNG, JPG, SVG, PDF) +- Access design tokens (colors, typography, spacing) +- Read component metadata and descriptions +- Retrieve version history + +**API Endpoints**: +``` +GET /v1/files/:file_key # Get file metadata +GET /v1/files/:file_key/nodes # Get specific nodes +GET /v1/images/:file_key # Export images +GET /v1/files/:file_key/components # Get components +GET /v1/files/:file_key/styles # Get styles +GET /v1/teams/:team_id/projects # List projects +GET /v1/files/:file_key/versions # Version history +``` + +**Authentication**: +```bash +# Personal Access Token (recommended for server-side) +curl -H "X-Figma-Token: YOUR_TOKEN" https://api.figma.com/v1/files/:file_key + +# OAuth 2.0 (recommended for user-facing apps) +# Authorization: Bearer YOUR_OAUTH_TOKEN +``` + +### 2. Setup Configuration + +**Environment Variables (.env)**: +```bash +# Required +FIGMA_ACCESS_TOKEN=figd_XXXXXXXXXXXXXXXXXXXX + +# Optional (MCP server) +FIGMA_MCP_ENABLED=true +FIGMA_MCP_SERVER_PATH=/path/to/figma-mcp-server +``` + +**Configuration File (figma.config.json)**: +```json +{ + "fileKey": "ABC123XYZ456", + "fileUrl": "https://www.figma.com/file/ABC123XYZ456/ProjectName", + "importSettings": { + "components": true, + "styles": true, + "assets": true, + "exportFormats": ["svg", "png@2x"], + "skipHidden": true, + "includeMetadata": true + }, + "exportPaths": { + "components": "./src/components/figma", + "assets": "./public/assets/figma", + "tokens": "./src/design-tokens", + "metadata": "./.figma/metadata.json" + }, + "naming": { + "componentPrefix": "Figma", + "useKebabCase": true, + "preserveFigmaNames": false + }, + "mcp": { + "enabled": false, + "serverPath": null, + "cacheDir": "./.figma/cache" + } +} +``` + +### 3. Figma REST API Integration + +**TypeScript/JavaScript Implementation**: + +```typescript +import axios from 'axios'; +import fs from 'fs/promises'; +import path from 'path'; + +interface FigmaConfig { + accessToken: string; + fileKey: string; + exportFormats?: string[]; +} + +class FigmaImporter { + private baseUrl = 'https://api.figma.com/v1'; + private headers: Record; + private fileKey: string; + + constructor(config: FigmaConfig) { + this.headers = { + 'X-Figma-Token': config.accessToken, + }; + this.fileKey = config.fileKey; + } + + /** + * Fetch file metadata and structure + */ + async fetchFile() { + const response = await axios.get( + `${this.baseUrl}/files/${this.fileKey}`, + { headers: this.headers } + ); + + return response.data; + } + + /** + * Fetch specific nodes by ID + */ + async fetchNodes(nodeIds: string[]) { + const ids = nodeIds.join(','); + const response = await axios.get( + `${this.baseUrl}/files/${this.fileKey}/nodes?ids=${ids}`, + { headers: this.headers } + ); + + return response.data.nodes; + } + + /** + * Export images for nodes + */ + async exportImages( + nodeIds: string[], + options: { + format?: 'png' | 'jpg' | 'svg' | 'pdf'; + scale?: number; + useAbsoluteBounds?: boolean; + } = {} + ) { + const { format = 'png', scale = 2, useAbsoluteBounds = false } = options; + const ids = nodeIds.join(','); + + const response = await axios.get( + `${this.baseUrl}/images/${this.fileKey}?ids=${ids}&format=${format}&scale=${scale}&use_absolute_bounds=${useAbsoluteBounds}`, + { headers: this.headers } + ); + + return response.data.images; + } + + /** + * Fetch all components in file + */ + async fetchComponents() { + const response = await axios.get( + `${this.baseUrl}/files/${this.fileKey}/components`, + { headers: this.headers } + ); + + return response.data.meta.components; + } + + /** + * Fetch all styles (colors, text, effects, grids) + */ + async fetchStyles() { + const response = await axios.get( + `${this.baseUrl}/files/${this.fileKey}/styles`, + { headers: this.headers } + ); + + return response.data.meta.styles; + } + + /** + * Download image from URL + */ + async downloadImage(imageUrl: string, outputPath: string) { + const response = await axios.get(imageUrl, { + responseType: 'arraybuffer', + }); + + await fs.mkdir(path.dirname(outputPath), { recursive: true }); + await fs.writeFile(outputPath, response.data); + + return outputPath; + } + + /** + * Traverse Figma node tree recursively + */ + traverseNodes(node: any, callback: (node: any) => void) { + callback(node); + + if (node.children) { + node.children.forEach((child: any) => { + this.traverseNodes(child, callback); + }); + } + } + + /** + * Extract design tokens from file + */ + async extractDesignTokens() { + const file = await this.fetchFile(); + const styles = await this.fetchStyles(); + + const tokens = { + colors: {}, + typography: {}, + spacing: {}, + effects: {}, + }; + + // Extract color tokens + styles.forEach((style: any) => { + if (style.style_type === 'FILL') { + const node = this.findNodeById(file.document, style.node_id); + if (node?.fills?.[0]) { + const color = node.fills[0]; + if (color.type === 'SOLID') { + tokens.colors[style.name] = this.rgbaToHex(color.color); + } + } + } + + // Extract text styles + if (style.style_type === 'TEXT') { + const node = this.findNodeById(file.document, style.node_id); + if (node?.style) { + tokens.typography[style.name] = { + fontFamily: node.style.fontFamily, + fontSize: node.style.fontSize, + fontWeight: node.style.fontWeight, + lineHeight: node.style.lineHeightPx, + letterSpacing: node.style.letterSpacing, + }; + } + } + + // Extract effect styles (shadows, blurs) + if (style.style_type === 'EFFECT') { + const node = this.findNodeById(file.document, style.node_id); + if (node?.effects) { + tokens.effects[style.name] = node.effects; + } + } + }); + + return tokens; + } + + /** + * Find node by ID in tree + */ + private findNodeById(node: any, id: string): any { + if (node.id === id) return node; + + if (node.children) { + for (const child of node.children) { + const found = this.findNodeById(child, id); + if (found) return found; + } + } + + return null; + } + + /** + * Convert RGBA to HEX + */ + private rgbaToHex(color: { r: number; g: number; b: number; a?: number }): string { + const r = Math.round(color.r * 255); + const g = Math.round(color.g * 255); + const b = Math.round(color.b * 255); + + return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; + } +} + +// Usage +async function importFigmaDesigns() { + const importer = new FigmaImporter({ + accessToken: process.env.FIGMA_ACCESS_TOKEN!, + fileKey: 'ABC123XYZ456', + }); + + // Fetch file structure + const file = await importer.fetchFile(); + console.log('File:', file.name); + + // Fetch components + const components = await importer.fetchComponents(); + console.log(`Found ${components.length} components`); + + // Export component images + const componentIds = components.map((c: any) => c.node_id); + const images = await importer.exportImages(componentIds, { + format: 'svg', + scale: 1, + }); + + // Download images + for (const [nodeId, imageUrl] of Object.entries(images)) { + const component = components.find((c: any) => c.node_id === nodeId); + const outputPath = `./assets/${component.name}.svg`; + await importer.downloadImage(imageUrl as string, outputPath); + console.log(`Exported: ${outputPath}`); + } + + // Extract design tokens + const tokens = await importer.extractDesignTokens(); + await fs.writeFile( + './src/design-tokens/figma-tokens.json', + JSON.stringify(tokens, null, 2) + ); +} + +importFigmaDesigns().catch(console.error); +``` + +### 4. MCP Server Integration + +**MCP Server for Figma** (Recommended for Claude Code): + +MCP (Model Context Protocol) servers provide structured access to Figma via Claude Code. + +**Available MCP Servers**: +1. **@modelcontextprotocol/server-figma** (Official) +2. **figma-mcp-server** (Community) +3. **claude-figma-bridge** (Custom) + +**Setup MCP Server**: + +```bash +# Install MCP server +npm install -g @modelcontextprotocol/server-figma + +# Configure in Claude Code settings +# ~/.claude/config.json +{ + "mcpServers": { + "figma": { + "command": "mcp-server-figma", + "args": [], + "env": { + "FIGMA_ACCESS_TOKEN": "figd_XXXXXXXXXXXXXXXXXXXX" + } + } + } +} +``` + +**Using MCP in Claude Code**: + +Once MCP server is configured, Claude Code can directly access Figma: + +```typescript +// Claude Code will use MCP tools automatically +// Example: mcp__figma__getFile, mcp__figma__exportImages + +// In your prompt: +// "Import all components from Figma file ABC123XYZ456 and export as SVG" + +// Claude Code will invoke: +// - mcp__figma__getFile({ fileKey: "ABC123XYZ456" }) +// - mcp__figma__exportImages({ nodeIds: [...], format: "svg" }) +``` + +**MCP Server Benefits**: +- No need to manage API tokens in code +- Automatic rate limiting +- Built-in caching +- Structured tool interface +- Better error handling + +### 5. Import Workflow + +**Step 1: Analyze Figma File** +```typescript +// Discover structure +const file = await importer.fetchFile(); + +// Find components, frames, pages +const components = []; +const frames = []; + +importer.traverseNodes(file.document, (node) => { + if (node.type === 'COMPONENT') { + components.push({ + id: node.id, + name: node.name, + description: node.description, + componentPropertyDefinitions: node.componentPropertyDefinitions, + }); + } + + if (node.type === 'FRAME') { + frames.push({ + id: node.id, + name: node.name, + width: node.absoluteBoundingBox.width, + height: node.absoluteBoundingBox.height, + }); + } +}); + +console.log(`Components: ${components.length}, Frames: ${frames.length}`); +``` + +**Step 2: Export Assets** +```typescript +// Export all components as SVG +const componentIds = components.map(c => c.id); +const svgImages = await importer.exportImages(componentIds, { + format: 'svg', + scale: 1, +}); + +// Export all frames as PNG@2x +const frameIds = frames.map(f => f.id); +const pngImages = await importer.exportImages(frameIds, { + format: 'png', + scale: 2, +}); +``` + +**Step 3: Save Metadata** +```typescript +const metadata = { + fileKey: importer.fileKey, + fileName: file.name, + version: file.version, + lastModified: file.lastModified, + components: components.map(c => ({ + id: c.id, + name: c.name, + description: c.description, + exportedAs: `./assets/components/${c.name}.svg`, + })), + frames: frames.map(f => ({ + id: f.id, + name: f.name, + dimensions: { width: f.width, height: f.height }, + exportedAs: `./assets/frames/${f.name}@2x.png`, + })), + importedAt: new Date().toISOString(), +}; + +await fs.writeFile( + './.figma/metadata.json', + JSON.stringify(metadata, null, 2) +); +``` + +### 6. CLI Command + +**NPM Script (package.json)**: +```json +{ + "scripts": { + "figma:import": "tsx scripts/figma-import.ts", + "figma:import:watch": "tsx scripts/figma-import.ts --watch", + "figma:sync": "tsx scripts/figma-sync.ts" + } +} +``` + +**CLI with Arguments**: +```typescript +// scripts/figma-import.ts +import yargs from 'yargs'; + +const argv = yargs(process.argv.slice(2)) + .option('file-key', { + type: 'string', + description: 'Figma file key', + required: true, + }) + .option('components', { + type: 'boolean', + description: 'Import components', + default: true, + }) + .option('assets', { + type: 'boolean', + description: 'Export assets', + default: true, + }) + .option('tokens', { + type: 'boolean', + description: 'Extract design tokens', + default: true, + }) + .option('format', { + type: 'string', + description: 'Export format (svg, png, jpg)', + default: 'svg', + }) + .parseSync(); + +const importer = new FigmaImporter({ + accessToken: process.env.FIGMA_ACCESS_TOKEN!, + fileKey: argv.fileKey, +}); + +if (argv.components) { + const components = await importer.fetchComponents(); + console.log(`Importing ${components.length} components...`); +} + +if (argv.assets) { + // Export logic +} + +if (argv.tokens) { + const tokens = await importer.extractDesignTokens(); + // Save tokens +} +``` + +**Usage**: +```bash +# Import everything +npm run figma:import -- --file-key ABC123XYZ456 + +# Import only components +npm run figma:import -- --file-key ABC123XYZ456 --no-assets --no-tokens + +# Export as PNG +npm run figma:import -- --file-key ABC123XYZ456 --format png +``` + +### 7. Rate Limiting and Caching + +**Rate Limits** (Figma API): +- 30 requests per minute per IP +- 1000 requests per hour per token + +**Implement Caching**: +```typescript +import NodeCache from 'node-cache'; + +class CachedFigmaImporter extends FigmaImporter { + private cache: NodeCache; + + constructor(config: FigmaConfig) { + super(config); + this.cache = new NodeCache({ stdTTL: 3600 }); // 1 hour TTL + } + + async fetchFile() { + const cacheKey = `file:${this.fileKey}`; + const cached = this.cache.get(cacheKey); + + if (cached) { + console.log('Using cached file data'); + return cached; + } + + const file = await super.fetchFile(); + this.cache.set(cacheKey, file); + return file; + } +} +``` + +**Rate Limiting**: +```typescript +import Bottleneck from 'bottleneck'; + +class RateLimitedFigmaImporter extends FigmaImporter { + private limiter: Bottleneck; + + constructor(config: FigmaConfig) { + super(config); + + // 30 requests per minute + this.limiter = new Bottleneck({ + minTime: 2000, // 2s between requests + maxConcurrent: 1, + }); + } + + async fetchFile() { + return this.limiter.schedule(() => super.fetchFile()); + } + + async exportImages(...args: any[]) { + return this.limiter.schedule(() => super.exportImages(...args)); + } +} +``` + +### 8. Error Handling + +**Common API Errors**: +- 401 Unauthorized: Invalid access token +- 403 Forbidden: No permission to access file +- 404 Not Found: File or node doesn't exist +- 429 Too Many Requests: Rate limit exceeded +- 500 Server Error: Figma API issue + +**Error Handling Pattern**: +```typescript +async function importWithRetry(fileKey: string, retries = 3) { + for (let i = 0; i < retries; i++) { + try { + const importer = new FigmaImporter({ + accessToken: process.env.FIGMA_ACCESS_TOKEN!, + fileKey, + }); + + return await importer.fetchFile(); + } catch (error) { + if (axios.isAxiosError(error)) { + const status = error.response?.status; + + if (status === 401) { + throw new Error('Invalid Figma access token'); + } + + if (status === 403) { + throw new Error('No permission to access this file'); + } + + if (status === 429) { + const retryAfter = error.response?.headers['retry-after'] || 60; + console.log(`Rate limited. Retrying after ${retryAfter}s...`); + await sleep(retryAfter * 1000); + continue; + } + + if (status === 500 && i < retries - 1) { + console.log(`Server error. Retrying (${i + 1}/${retries})...`); + await sleep(2000); + continue; + } + } + + throw error; + } + } + + throw new Error('Max retries exceeded'); +} +``` + +## Workflow + +1. Ask about Figma file URL and access requirements +2. Extract file key from URL +3. Verify Figma access token (from .env or prompt) +4. Fetch file metadata and analyze structure +5. Ask about import preferences (components, assets, tokens) +6. Set up directory structure for exports +7. Export assets with specified formats and scales +8. Extract design tokens and metadata +9. Save all data to configured paths +10. Generate import report with statistics + +## When to Use + +- Starting new projects with Figma designs +- Syncing design systems with code +- Automating asset exports from Figma +- Extracting design tokens for theme configuration +- Migrating from manual Figma exports to automated workflow +- Setting up CI/CD for design-to-code pipeline + +## Best Practices + +1. **Security**: Store access tokens in .env (never commit) +2. **Caching**: Cache file data to reduce API calls +3. **Rate Limiting**: Respect Figma API limits (30 req/min) +4. **Versioning**: Track Figma file versions in metadata +5. **Organization**: Structure exports by component type +6. **Documentation**: Include component descriptions from Figma +7. **Automation**: Use watch mode for continuous sync +8. **Testing**: Validate exports before committing + +Import Figma designs with production-ready automation! diff --git a/commands/figma-to-react.md b/commands/figma-to-react.md new file mode 100644 index 0000000..a5ab7cc --- /dev/null +++ b/commands/figma-to-react.md @@ -0,0 +1,834 @@ +# /specweave-figma:to-react + +Convert Figma components to production-ready React components with TypeScript, styled-components, and responsive design. + +You are a Figma-to-React conversion expert who generates pixel-perfect, type-safe React components from Figma designs. + +## Your Task + +Transform Figma components into React components with proper TypeScript types, styling, accessibility, and responsive behavior. + +### 1. Conversion Architecture + +**Design-to-Code Pipeline**: +``` +Figma Component → Analyze Structure → Extract Props → Generate TSX → Apply Styles → Add Interactivity +``` + +**Supported Patterns**: +- Functional components with hooks +- TypeScript interfaces for props +- Styled-components or CSS modules +- Responsive design (mobile-first) +- Accessibility attributes (ARIA) +- Component variants (Figma properties) +- Auto-layout → Flexbox/Grid + +### 2. Component Analysis + +**Figma Node Structure**: +```typescript +interface FigmaNode { + id: string; + name: string; + type: 'COMPONENT' | 'FRAME' | 'TEXT' | 'RECTANGLE' | 'VECTOR'; + children?: FigmaNode[]; + + // Layout + absoluteBoundingBox: { x: number; y: number; width: number; height: number }; + layoutMode?: 'HORIZONTAL' | 'VERTICAL' | 'NONE'; + layoutAlign?: 'MIN' | 'CENTER' | 'MAX' | 'STRETCH'; + primaryAxisSizingMode?: 'FIXED' | 'AUTO'; + counterAxisSizingMode?: 'FIXED' | 'AUTO'; + paddingLeft?: number; + paddingRight?: number; + paddingTop?: number; + paddingBottom?: number; + itemSpacing?: number; + + // Styling + fills?: Fill[]; + strokes?: Stroke[]; + effects?: Effect[]; + cornerRadius?: number; + + // Text + characters?: string; + style?: TextStyle; + + // Component properties (variants) + componentPropertyDefinitions?: Record; +} +``` + +**Extract Component Metadata**: +```typescript +function analyzeComponent(node: FigmaNode) { + return { + name: node.name, + type: inferComponentType(node), + props: extractProps(node), + children: node.children?.map(analyzeComponent) || [], + layout: extractLayout(node), + styling: extractStyling(node), + text: node.type === 'TEXT' ? node.characters : null, + variants: node.componentPropertyDefinitions || {}, + }; +} + +function inferComponentType(node: FigmaNode): string { + // Button detection + if (node.name.toLowerCase().includes('button')) return 'Button'; + + // Input detection + if (node.name.toLowerCase().includes('input')) return 'Input'; + + // Card detection + if (node.name.toLowerCase().includes('card')) return 'Card'; + + // Icon detection + if (node.type === 'VECTOR') return 'Icon'; + + // Text detection + if (node.type === 'TEXT') return 'Text'; + + // Generic container + return 'Container'; +} +``` + +### 3. React Component Generation + +**TypeScript Component Template**: + +```typescript +import { FC } from 'react'; +import styled from 'styled-components'; + +// Generated interfaces from Figma component properties +interface ${ComponentName}Props { + variant?: 'primary' | 'secondary' | 'tertiary'; + size?: 'small' | 'medium' | 'large'; + disabled?: boolean; + onClick?: () => void; + children?: React.ReactNode; +} + +const ${ComponentName}: FC<${ComponentName}Props> = ({ + variant = 'primary', + size = 'medium', + disabled = false, + onClick, + children, +}) => { + return ( + + {children} + + ); +}; + +const StyledContainer = styled.div<${ComponentName}Props>` + /* Generated styles from Figma */ +`; + +export default ${ComponentName}; +``` + +**Complete Example - Button Component**: + +```typescript +import { FC, ButtonHTMLAttributes } from 'react'; +import styled from 'styled-components'; + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: 'primary' | 'secondary' | 'outline'; + size?: 'sm' | 'md' | 'lg'; + fullWidth?: boolean; + leftIcon?: React.ReactNode; + rightIcon?: React.ReactNode; + loading?: boolean; +} + +const Button: FC = ({ + variant = 'primary', + size = 'md', + fullWidth = false, + leftIcon, + rightIcon, + loading = false, + disabled, + children, + ...props +}) => { + return ( + + {leftIcon && {leftIcon}} + {loading ? : children} + {rightIcon && {rightIcon}} + + ); +}; + +const StyledButton = styled.button` + /* Base styles */ + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + font-family: 'Inter', sans-serif; + font-weight: 500; + border: none; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; + + /* Full width */ + width: ${({ fullWidth }) => fullWidth ? '100%' : 'auto'}; + + /* Size variants */ + ${({ size }) => { + switch (size) { + case 'sm': + return ` + padding: 8px 16px; + font-size: 14px; + line-height: 20px; + `; + case 'lg': + return ` + padding: 16px 32px; + font-size: 18px; + line-height: 24px; + `; + default: // md + return ` + padding: 12px 24px; + font-size: 16px; + line-height: 24px; + `; + } + }} + + /* Color variants */ + ${({ variant }) => { + switch (variant) { + case 'primary': + return ` + background: #0066FF; + color: #FFFFFF; + + &:hover:not(:disabled) { + background: #0052CC; + } + + &:active:not(:disabled) { + background: #003D99; + } + `; + case 'secondary': + return ` + background: #F0F0F0; + color: #333333; + + &:hover:not(:disabled) { + background: #E0E0E0; + } + + &:active:not(:disabled) { + background: #D0D0D0; + } + `; + case 'outline': + return ` + background: transparent; + color: #0066FF; + border: 2px solid #0066FF; + + &:hover:not(:disabled) { + background: rgba(0, 102, 255, 0.1); + } + + &:active:not(:disabled) { + background: rgba(0, 102, 255, 0.2); + } + `; + } + }} + + /* Disabled state */ + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + /* Focus state (accessibility) */ + &:focus-visible { + outline: 2px solid #0066FF; + outline-offset: 2px; + } +`; + +const IconWrapper = styled.span` + display: inline-flex; + align-items: center; +`; + +const Spinner = styled.div` + width: 16px; + height: 16px; + border: 2px solid currentColor; + border-right-color: transparent; + border-radius: 50%; + animation: spin 0.6s linear infinite; + + @keyframes spin { + to { transform: rotate(360deg); } + } +`; + +export default Button; +``` + +### 4. Style Conversion + +**Figma → CSS Mapping**: + +```typescript +function convertFigmaStylesToCSS(node: FigmaNode): string { + const styles: string[] = []; + + // Layout (Auto Layout → Flexbox) + if (node.layoutMode) { + styles.push('display: flex;'); + styles.push(`flex-direction: ${node.layoutMode === 'HORIZONTAL' ? 'row' : 'column'};`); + + if (node.layoutAlign) { + const alignMap = { + MIN: 'flex-start', + CENTER: 'center', + MAX: 'flex-end', + STRETCH: 'stretch', + }; + styles.push(`align-items: ${alignMap[node.layoutAlign]};`); + } + + if (node.itemSpacing) { + styles.push(`gap: ${node.itemSpacing}px;`); + } + } + + // Padding + if (node.paddingLeft || node.paddingRight || node.paddingTop || node.paddingBottom) { + const padding = [ + node.paddingTop || 0, + node.paddingRight || 0, + node.paddingBottom || 0, + node.paddingLeft || 0, + ].join('px '); + styles.push(`padding: ${padding}px;`); + } + + // Size + const box = node.absoluteBoundingBox; + if (node.primaryAxisSizingMode === 'FIXED') { + const dim = node.layoutMode === 'HORIZONTAL' ? 'width' : 'height'; + styles.push(`${dim}: ${box.width}px;`); + } + + // Background (fills) + if (node.fills && node.fills.length > 0) { + const fill = node.fills[0]; + if (fill.type === 'SOLID') { + const color = rgbaToCSS(fill.color, fill.opacity); + styles.push(`background: ${color};`); + } else if (fill.type === 'GRADIENT_LINEAR') { + const gradient = convertGradient(fill); + styles.push(`background: ${gradient};`); + } + } + + // Border (strokes) + if (node.strokes && node.strokes.length > 0) { + const stroke = node.strokes[0]; + const color = rgbaToCSS(stroke.color, stroke.opacity); + const width = node.strokeWeight || 1; + styles.push(`border: ${width}px solid ${color};`); + } + + // Border radius + if (node.cornerRadius) { + styles.push(`border-radius: ${node.cornerRadius}px;`); + } + + // Shadows (effects) + if (node.effects && node.effects.length > 0) { + const shadows = node.effects + .filter((e: any) => e.type === 'DROP_SHADOW') + .map((e: any) => { + const color = rgbaToCSS(e.color, e.color.a); + return `${e.offset.x}px ${e.offset.y}px ${e.radius}px ${color}`; + }) + .join(', '); + + if (shadows) { + styles.push(`box-shadow: ${shadows};`); + } + } + + // Typography (text styles) + if (node.style) { + styles.push(`font-family: '${node.style.fontFamily}', sans-serif;`); + styles.push(`font-size: ${node.style.fontSize}px;`); + styles.push(`font-weight: ${node.style.fontWeight};`); + styles.push(`line-height: ${node.style.lineHeightPx}px;`); + + if (node.style.letterSpacing) { + styles.push(`letter-spacing: ${node.style.letterSpacing}px;`); + } + + if (node.style.textAlignHorizontal) { + const alignMap = { LEFT: 'left', CENTER: 'center', RIGHT: 'right', JUSTIFIED: 'justify' }; + styles.push(`text-align: ${alignMap[node.style.textAlignHorizontal]};`); + } + } + + return styles.join('\n '); +} + +function rgbaToCSS(color: { r: number; g: number; b: number }, opacity = 1): string { + const r = Math.round(color.r * 255); + const g = Math.round(color.g * 255); + const b = Math.round(color.b * 255); + + return opacity === 1 + ? `rgb(${r}, ${g}, ${b})` + : `rgba(${r}, ${g}, ${b}, ${opacity})`; +} + +function convertGradient(fill: any): string { + const stops = fill.gradientStops + .map((stop: any) => { + const color = rgbaToCSS(stop.color, stop.color.a); + return `${color} ${Math.round(stop.position * 100)}%`; + }) + .join(', '); + + const angle = Math.atan2( + fill.gradientHandlePositions[1].y - fill.gradientHandlePositions[0].y, + fill.gradientHandlePositions[1].x - fill.gradientHandlePositions[0].x + ) * (180 / Math.PI); + + return `linear-gradient(${angle}deg, ${stops})`; +} +``` + +### 5. Responsive Design + +**Generate Media Queries from Figma Breakpoints**: + +```typescript +const breakpoints = { + mobile: 375, + tablet: 768, + desktop: 1440, +}; + +const StyledCard = styled.div` + /* Mobile-first base styles */ + padding: 16px; + font-size: 14px; + + /* Tablet */ + @media (min-width: ${breakpoints.tablet}px) { + padding: 24px; + font-size: 16px; + } + + /* Desktop */ + @media (min-width: ${breakpoints.desktop}px) { + padding: 32px; + font-size: 18px; + } +`; +``` + +**Responsive Container**: +```typescript +const Container = styled.div` + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 0 16px; + + @media (min-width: 768px) { + padding: 0 32px; + } + + @media (min-width: 1440px) { + padding: 0 64px; + } +`; +``` + +### 6. Component Variants (Figma Properties) + +**Figma Component Property → React Props**: + +```typescript +// Figma component with variants: +// - State: Default, Hover, Active, Disabled +// - Size: Small, Medium, Large +// - Icon: None, Left, Right + +interface ChipProps { + state?: 'default' | 'hover' | 'active' | 'disabled'; + size?: 'sm' | 'md' | 'lg'; + iconPosition?: 'none' | 'left' | 'right'; + label: string; + icon?: React.ReactNode; + onClick?: () => void; +} + +const Chip: FC = ({ + state = 'default', + size = 'md', + iconPosition = 'none', + label, + icon, + onClick, +}) => { + return ( + + {iconPosition === 'left' && icon && {icon}} + + {iconPosition === 'right' && icon && {icon}} + + ); +}; + +const StyledChip = styled.div` + display: inline-flex; + align-items: center; + gap: 8px; + border-radius: 100px; + font-weight: 500; + cursor: ${({ state }) => state === 'disabled' ? 'not-allowed' : 'pointer'}; + transition: all 0.2s; + + /* Size variants */ + ${({ size }) => { + switch (size) { + case 'sm': return `padding: 4px 12px; font-size: 12px;`; + case 'lg': return `padding: 12px 20px; font-size: 16px;`; + default: return `padding: 8px 16px; font-size: 14px;`; + } + }} + + /* State variants */ + ${({ state }) => { + switch (state) { + case 'hover': + return `background: #E0F2FF; color: #0066FF;`; + case 'active': + return `background: #0066FF; color: #FFFFFF;`; + case 'disabled': + return `background: #F0F0F0; color: #A0A0A0; opacity: 0.6;`; + default: + return `background: #F0F0F0; color: #333333;`; + } + }} +`; +``` + +### 7. Accessibility (a11y) + +**Add ARIA Attributes**: + +```typescript +const AccessibleButton: FC = ({ + children, + disabled, + loading, + ariaLabel, + ...props +}) => { + return ( + + ); +}; + +// Icon buttons MUST have aria-label +const IconButton: FC = ({ icon, onClick, ariaLabel }) => { + return ( + + ); +}; +``` + +**Semantic HTML**: +```typescript +// ❌ Wrong: div soup +
Submit
+ +// ✅ Correct: semantic button + + +// ❌ Wrong: generic div +
Card Title
+ +// ✅ Correct: heading +

Card Title

+``` + +### 8. Storybook Integration + +**Generate Storybook Stories**: + +```typescript +// Button.stories.tsx +import type { Meta, StoryObj } from '@storybook/react'; +import Button from './Button'; + +const meta: Meta = { + title: 'Components/Button', + component: Button, + tags: ['autodocs'], + argTypes: { + variant: { + control: 'select', + options: ['primary', 'secondary', 'outline'], + }, + size: { + control: 'select', + options: ['sm', 'md', 'lg'], + }, + disabled: { + control: 'boolean', + }, + loading: { + control: 'boolean', + }, + fullWidth: { + control: 'boolean', + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + variant: 'primary', + size: 'md', + children: 'Button', + }, +}; + +export const Secondary: Story = { + args: { + variant: 'secondary', + size: 'md', + children: 'Button', + }, +}; + +export const WithIcons: Story = { + args: { + variant: 'primary', + size: 'md', + leftIcon: , + rightIcon: , + children: 'Button', + }, +}; + +export const Loading: Story = { + args: { + variant: 'primary', + size: 'md', + loading: true, + children: 'Button', + }, +}; + +export const Disabled: Story = { + args: { + variant: 'primary', + size: 'md', + disabled: true, + children: 'Button', + }, +}; +``` + +### 9. Testing + +**Generate Component Tests**: + +```typescript +import { render, screen, fireEvent } from '@testing-library/react'; +import Button from './Button'; + +describe('Button', () => { + it('renders children correctly', () => { + render(); + expect(screen.getByText('Click me')).toBeInTheDocument(); + }); + + it('calls onClick when clicked', () => { + const handleClick = vi.fn(); + render(); + + fireEvent.click(screen.getByText('Click me')); + expect(handleClick).toHaveBeenCalledTimes(1); + }); + + it('does not call onClick when disabled', () => { + const handleClick = vi.fn(); + render(); + + fireEvent.click(screen.getByText('Click me')); + expect(handleClick).not.toHaveBeenCalled(); + }); + + it('renders loading state correctly', () => { + render(); + expect(screen.queryByText('Click me')).not.toBeInTheDocument(); + }); + + it('renders icons correctly', () => { + render( + + ); + + expect(screen.getByTestId('left-icon')).toBeInTheDocument(); + }); +}); +``` + +### 10. Code Generation Automation + +**Full Pipeline Script**: + +```typescript +import { FigmaImporter } from './figma-importer'; +import { generateReactComponent } from './react-generator'; +import fs from 'fs/promises'; + +async function figmaToReact(fileKey: string, componentName: string) { + // 1. Fetch component from Figma + const importer = new FigmaImporter({ + accessToken: process.env.FIGMA_ACCESS_TOKEN!, + fileKey, + }); + + const file = await importer.fetchFile(); + const component = findComponentByName(file.document, componentName); + + if (!component) { + throw new Error(`Component "${componentName}" not found`); + } + + // 2. Analyze component structure + const analysis = analyzeComponent(component); + + // 3. Generate React component code + const reactCode = generateReactComponent(analysis); + + // 4. Generate TypeScript types + const types = generateTypeScriptTypes(analysis); + + // 5. Generate styled-components + const styles = generateStyledComponents(analysis); + + // 6. Generate Storybook story + const story = generateStorybook(analysis); + + // 7. Generate tests + const tests = generateTests(analysis); + + // 8. Save files + const componentDir = `./src/components/${analysis.name}`; + await fs.mkdir(componentDir, { recursive: true }); + + await fs.writeFile(`${componentDir}/${analysis.name}.tsx`, reactCode); + await fs.writeFile(`${componentDir}/${analysis.name}.types.ts`, types); + await fs.writeFile(`${componentDir}/${analysis.name}.styles.ts`, styles); + await fs.writeFile(`${componentDir}/${analysis.name}.stories.tsx`, story); + await fs.writeFile(`${componentDir}/${analysis.name}.test.tsx`, tests); + await fs.writeFile(`${componentDir}/index.ts`, `export { default } from './${analysis.name}';`); + + console.log(`✅ Generated React component: ${analysis.name}`); + console.log(`📁 Location: ${componentDir}`); +} + +// Usage +figmaToReact('ABC123XYZ456', 'Button').catch(console.error); +``` + +## Workflow + +1. Ask about Figma file and component to convert +2. Fetch component metadata from Figma API +3. Analyze component structure and variants +4. Ask about styling approach (styled-components, CSS modules, Tailwind) +5. Generate TypeScript component with props interface +6. Convert Figma styles to CSS/styled-components +7. Add responsive breakpoints if needed +8. Generate Storybook stories for all variants +9. Generate unit tests +10. Save all generated files and provide usage examples + +## When to Use + +- Converting Figma designs to React components +- Building design systems from Figma +- Automating component creation from mockups +- Ensuring pixel-perfect implementation +- Syncing Figma variants with React props +- Generating Storybook documentation from designs + +## Best Practices + +1. **Type Safety**: Always generate TypeScript interfaces +2. **Variants**: Map Figma component properties to React props +3. **Accessibility**: Include ARIA attributes and semantic HTML +4. **Responsive**: Generate mobile-first responsive styles +5. **Testing**: Create comprehensive unit tests +6. **Documentation**: Generate Storybook stories automatically +7. **Naming**: Use PascalCase for components, match Figma names +8. **Optimization**: Extract common styles to theme tokens + +Transform Figma designs into production-ready React components! diff --git a/commands/figma-tokens.md b/commands/figma-tokens.md new file mode 100644 index 0000000..53e8780 --- /dev/null +++ b/commands/figma-tokens.md @@ -0,0 +1,815 @@ +# /specweave-figma:tokens + +Extract design tokens from Figma and generate token files for theme configuration (CSS variables, JavaScript, JSON, SCSS). + +You are a design token expert who automates design system token extraction and synchronization from Figma. + +## Your Task + +Extract design tokens (colors, typography, spacing, shadows, borders) from Figma and generate production-ready token files in multiple formats. + +### 1. Design Token Standards + +**W3C Design Tokens Format** (Recommended): +```json +{ + "colors": { + "brand": { + "primary": { + "$type": "color", + "$value": "#0066FF", + "$description": "Primary brand color" + }, + "secondary": { + "$type": "color", + "$value": "#00CC66" + } + } + }, + "typography": { + "heading": { + "h1": { + "$type": "typography", + "$value": { + "fontFamily": "Inter", + "fontSize": "48px", + "fontWeight": 700, + "lineHeight": 1.2 + } + } + } + } +} +``` + +**Token Categories**: +- Colors (brand, semantic, grayscale) +- Typography (font families, sizes, weights, line heights) +- Spacing (margins, paddings, gaps) +- Border radius (corners) +- Shadows (elevations) +- Borders (widths, styles) +- Breakpoints (responsive) +- Z-index (layering) +- Transitions (animations) + +### 2. Figma Token Extraction + +**Extract Color Tokens from Styles**: + +```typescript +import { FigmaImporter } from './figma-importer'; + +interface ColorToken { + name: string; + value: string; + rgba: { r: number; g: number; b: number; a: number }; + type: 'solid' | 'gradient'; + description?: string; +} + +async function extractColorTokens(fileKey: string): Promise { + const importer = new FigmaImporter({ + accessToken: process.env.FIGMA_ACCESS_TOKEN!, + fileKey, + }); + + const file = await importer.fetchFile(); + const styles = await importer.fetchStyles(); + + const colorTokens: ColorToken[] = []; + + for (const style of styles) { + if (style.style_type === 'FILL') { + const node = findNodeById(file.document, style.node_id); + + if (node?.fills?.[0]) { + const fill = node.fills[0]; + + if (fill.type === 'SOLID') { + colorTokens.push({ + name: style.name, + value: rgbaToHex(fill.color, fill.opacity), + rgba: { + r: fill.color.r, + g: fill.color.g, + b: fill.color.b, + a: fill.opacity ?? 1, + }, + type: 'solid', + description: style.description || undefined, + }); + } else if (fill.type === 'GRADIENT_LINEAR') { + colorTokens.push({ + name: style.name, + value: convertGradientToCSS(fill), + rgba: fill.gradientStops[0].color, + type: 'gradient', + description: style.description, + }); + } + } + } + } + + return colorTokens; +} + +function rgbaToHex(color: { r: number; g: number; b: number }, opacity = 1): string { + const r = Math.round(color.r * 255); + const g = Math.round(color.g * 255); + const b = Math.round(color.b * 255); + + if (opacity < 1) { + const a = Math.round(opacity * 255); + return `#${[r, g, b, a].map(x => x.toString(16).padStart(2, '0')).join('')}`; + } + + return `#${[r, g, b].map(x => x.toString(16).padStart(2, '0')).join('')}`; +} +``` + +**Extract Typography Tokens**: + +```typescript +interface TypographyToken { + name: string; + fontFamily: string; + fontSize: number; + fontWeight: number; + lineHeight: number; + letterSpacing?: number; + textTransform?: 'none' | 'uppercase' | 'lowercase' | 'capitalize'; + description?: string; +} + +async function extractTypographyTokens(fileKey: string): Promise { + const importer = new FigmaImporter({ + accessToken: process.env.FIGMA_ACCESS_TOKEN!, + fileKey, + }); + + const file = await importer.fetchFile(); + const styles = await importer.fetchStyles(); + + const typographyTokens: TypographyToken[] = []; + + for (const style of styles) { + if (style.style_type === 'TEXT') { + const node = findNodeById(file.document, style.node_id); + + if (node?.style) { + typographyTokens.push({ + name: style.name, + fontFamily: node.style.fontFamily, + fontSize: node.style.fontSize, + fontWeight: node.style.fontWeight, + lineHeight: node.style.lineHeightPx / node.style.fontSize, + letterSpacing: node.style.letterSpacing || undefined, + textTransform: node.style.textCase?.toLowerCase() as any, + description: style.description, + }); + } + } + } + + return typographyTokens; +} +``` + +**Extract Spacing Tokens from Auto Layout**: + +```typescript +interface SpacingToken { + name: string; + value: number; + description?: string; +} + +async function extractSpacingTokens(fileKey: string): Promise { + const importer = new FigmaImporter({ + accessToken: process.env.FIGMA_ACCESS_TOKEN!, + fileKey, + }); + + const file = await importer.fetchFile(); + + const spacingValues = new Set(); + + // Traverse all frames with auto layout + traverseNodes(file.document, (node) => { + if (node.layoutMode) { + // Item spacing (gap) + if (node.itemSpacing) { + spacingValues.add(node.itemSpacing); + } + + // Padding + if (node.paddingLeft) spacingValues.add(node.paddingLeft); + if (node.paddingRight) spacingValues.add(node.paddingRight); + if (node.paddingTop) spacingValues.add(node.paddingTop); + if (node.paddingBottom) spacingValues.add(node.paddingBottom); + } + }); + + // Generate semantic names (4px → xs, 8px → sm, etc.) + const sortedSpacing = Array.from(spacingValues).sort((a, b) => a - b); + + const sizeMap = ['xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl', '4xl']; + + return sortedSpacing.map((value, index) => ({ + name: sizeMap[index] || `spacing-${index}`, + value, + description: `${value}px spacing`, + })); +} +``` + +**Extract Shadow Tokens (Elevation)**: + +```typescript +interface ShadowToken { + name: string; + value: string; // CSS box-shadow value + layers: Array<{ + x: number; + y: number; + blur: number; + spread: number; + color: string; + }>; + description?: string; +} + +async function extractShadowTokens(fileKey: string): Promise { + const importer = new FigmaImporter({ + accessToken: process.env.FIGMA_ACCESS_TOKEN!, + fileKey, + }); + + const file = await importer.fetchFile(); + const styles = await importer.fetchStyles(); + + const shadowTokens: ShadowToken[] = []; + + for (const style of styles) { + if (style.style_type === 'EFFECT') { + const node = findNodeById(file.document, style.node_id); + + if (node?.effects) { + const shadows = node.effects.filter( + (e: any) => e.type === 'DROP_SHADOW' || e.type === 'INNER_SHADOW' + ); + + if (shadows.length > 0) { + const layers = shadows.map((shadow: any) => ({ + x: shadow.offset.x, + y: shadow.offset.y, + blur: shadow.radius, + spread: shadow.spread || 0, + color: rgbaToHex(shadow.color, shadow.color.a), + })); + + const cssValue = layers + .map( + (layer) => + `${layer.x}px ${layer.y}px ${layer.blur}px ${layer.spread}px ${layer.color}` + ) + .join(', '); + + shadowTokens.push({ + name: style.name, + value: cssValue, + layers, + description: style.description, + }); + } + } + } + } + + return shadowTokens; +} +``` + +### 3. Token Organization + +**Hierarchical Token Structure**: + +``` +tokens/ +├── global/ +│ ├── colors.json # Brand colors, primitives +│ ├── typography.json # Font scales +│ ├── spacing.json # Spacing scale +│ └── shadows.json # Elevation scale +├── semantic/ +│ ├── colors.json # Semantic colors (success, error, warning) +│ ├── components.json # Component-specific tokens +│ └── themes.json # Light/dark theme mappings +└── platform/ + ├── web.json # Web-specific tokens (CSS variables) + ├── ios.json # iOS-specific (Swift) + └── android.json # Android-specific (XML) +``` + +**Naming Convention** (BEM-inspired): +``` +category-element-modifier-state + +Examples: +- color-brand-primary +- color-semantic-success +- color-text-primary +- typography-heading-h1 +- spacing-component-button-padding +- shadow-elevation-1 +- border-radius-sm +``` + +### 4. Token File Formats + +**CSS Variables** (Most Common): + +```css +/* tokens/global/colors.css */ +:root { + /* Brand Colors */ + --color-brand-primary: #0066FF; + --color-brand-secondary: #00CC66; + --color-brand-tertiary: #FF6600; + + /* Semantic Colors */ + --color-semantic-success: #00CC66; + --color-semantic-error: #FF3333; + --color-semantic-warning: #FFCC00; + --color-semantic-info: #0066FF; + + /* Grayscale */ + --color-gray-50: #F9FAFB; + --color-gray-100: #F3F4F6; + --color-gray-200: #E5E7EB; + --color-gray-300: #D1D5DB; + --color-gray-400: #9CA3AF; + --color-gray-500: #6B7280; + --color-gray-600: #4B5563; + --color-gray-700: #374151; + --color-gray-800: #1F2937; + --color-gray-900: #111827; + + /* Typography */ + --font-family-sans: 'Inter', -apple-system, sans-serif; + --font-family-mono: 'JetBrains Mono', monospace; + + --font-size-xs: 0.75rem; /* 12px */ + --font-size-sm: 0.875rem; /* 14px */ + --font-size-base: 1rem; /* 16px */ + --font-size-lg: 1.125rem; /* 18px */ + --font-size-xl: 1.25rem; /* 20px */ + --font-size-2xl: 1.5rem; /* 24px */ + --font-size-3xl: 1.875rem; /* 30px */ + --font-size-4xl: 2.25rem; /* 36px */ + + --font-weight-normal: 400; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + + --line-height-tight: 1.25; + --line-height-normal: 1.5; + --line-height-relaxed: 1.75; + + /* Spacing */ + --spacing-xs: 0.25rem; /* 4px */ + --spacing-sm: 0.5rem; /* 8px */ + --spacing-md: 1rem; /* 16px */ + --spacing-lg: 1.5rem; /* 24px */ + --spacing-xl: 2rem; /* 32px */ + --spacing-2xl: 3rem; /* 48px */ + --spacing-3xl: 4rem; /* 64px */ + + /* Shadows */ + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1); + + /* Border Radius */ + --radius-sm: 0.25rem; /* 4px */ + --radius-md: 0.5rem; /* 8px */ + --radius-lg: 0.75rem; /* 12px */ + --radius-xl: 1rem; /* 16px */ + --radius-full: 9999px; + + /* Transitions */ + --transition-fast: 150ms; + --transition-base: 250ms; + --transition-slow: 350ms; +} +``` + +**JavaScript/TypeScript**: + +```typescript +// tokens/global/colors.ts +export const colors = { + brand: { + primary: '#0066FF', + secondary: '#00CC66', + tertiary: '#FF6600', + }, + semantic: { + success: '#00CC66', + error: '#FF3333', + warning: '#FFCC00', + info: '#0066FF', + }, + gray: { + 50: '#F9FAFB', + 100: '#F3F4F6', + 200: '#E5E7EB', + 300: '#D1D5DB', + 400: '#9CA3AF', + 500: '#6B7280', + 600: '#4B5563', + 700: '#374151', + 800: '#1F2937', + 900: '#111827', + }, +} as const; + +// tokens/global/typography.ts +export const typography = { + fontFamily: { + sans: "'Inter', -apple-system, sans-serif", + mono: "'JetBrains Mono', monospace", + }, + fontSize: { + xs: '0.75rem', + sm: '0.875rem', + base: '1rem', + lg: '1.125rem', + xl: '1.25rem', + '2xl': '1.5rem', + '3xl': '1.875rem', + '4xl': '2.25rem', + }, + fontWeight: { + normal: 400, + medium: 500, + semibold: 600, + bold: 700, + }, + lineHeight: { + tight: 1.25, + normal: 1.5, + relaxed: 1.75, + }, +} as const; + +// tokens/index.ts +export { colors } from './global/colors'; +export { typography } from './global/typography'; +export { spacing } from './global/spacing'; +export { shadows } from './global/shadows'; + +// Type-safe token access +export type ColorToken = keyof typeof colors.brand | keyof typeof colors.semantic; +``` + +**JSON** (W3C Design Tokens Format): + +```json +{ + "$schema": "https://design-tokens.org/schemas/v1.0.0/design-tokens.schema.json", + "colors": { + "brand": { + "primary": { + "$type": "color", + "$value": "#0066FF", + "$description": "Primary brand color used for main actions and links" + }, + "secondary": { + "$type": "color", + "$value": "#00CC66", + "$description": "Secondary brand color for accents" + } + }, + "semantic": { + "success": { + "$type": "color", + "$value": "{colors.brand.secondary}", + "$description": "Success state color (references brand.secondary)" + } + } + }, + "typography": { + "heading": { + "h1": { + "$type": "typography", + "$value": { + "fontFamily": "{typography.fontFamily.sans}", + "fontSize": "{typography.fontSize.4xl}", + "fontWeight": "{typography.fontWeight.bold}", + "lineHeight": "{typography.lineHeight.tight}" + } + } + } + } +} +``` + +**SCSS Variables**: + +```scss +// tokens/global/_colors.scss +$color-brand-primary: #0066FF; +$color-brand-secondary: #00CC66; + +$color-gray-50: #F9FAFB; +$color-gray-100: #F3F4F6; +// ... + +// tokens/global/_typography.scss +$font-family-sans: 'Inter', -apple-system, sans-serif; +$font-size-base: 1rem; +$font-weight-bold: 700; + +// tokens/_index.scss +@forward 'global/colors'; +@forward 'global/typography'; +@forward 'global/spacing'; +``` + +**Tailwind CSS Config**: + +```javascript +// tailwind.config.js +const { colors, typography, spacing, shadows } = require('./tokens'); + +module.exports = { + theme: { + extend: { + colors: { + brand: colors.brand, + semantic: colors.semantic, + gray: colors.gray, + }, + fontFamily: typography.fontFamily, + fontSize: typography.fontSize, + fontWeight: typography.fontWeight, + lineHeight: typography.lineHeight, + spacing: spacing, + boxShadow: shadows, + }, + }, +}; +``` + +### 5. Token Generation Automation + +**Complete Pipeline**: + +```typescript +import fs from 'fs/promises'; +import path from 'path'; + +interface TokenGeneratorConfig { + fileKey: string; + outputDir: string; + formats: Array<'css' | 'scss' | 'js' | 'ts' | 'json' | 'tailwind'>; +} + +async function generateTokens(config: TokenGeneratorConfig) { + const { fileKey, outputDir, formats } = config; + + // 1. Extract tokens from Figma + const colorTokens = await extractColorTokens(fileKey); + const typographyTokens = await extractTypographyTokens(fileKey); + const spacingTokens = await extractSpacingTokens(fileKey); + const shadowTokens = await extractShadowTokens(fileKey); + + const allTokens = { + colors: colorTokens, + typography: typographyTokens, + spacing: spacingTokens, + shadows: shadowTokens, + }; + + // 2. Create output directory + await fs.mkdir(outputDir, { recursive: true }); + + // 3. Generate formats + if (formats.includes('css')) { + const css = generateCSSVariables(allTokens); + await fs.writeFile(path.join(outputDir, 'tokens.css'), css); + } + + if (formats.includes('scss')) { + const scss = generateSCSSVariables(allTokens); + await fs.writeFile(path.join(outputDir, '_tokens.scss'), scss); + } + + if (formats.includes('js') || formats.includes('ts')) { + const js = generateJavaScript(allTokens); + const ext = formats.includes('ts') ? '.ts' : '.js'; + await fs.writeFile(path.join(outputDir, `tokens${ext}`), js); + } + + if (formats.includes('json')) { + const json = generateW3CTokens(allTokens); + await fs.writeFile(path.join(outputDir, 'tokens.json'), json); + } + + if (formats.includes('tailwind')) { + const tailwind = generateTailwindConfig(allTokens); + await fs.writeFile(path.join(outputDir, 'tailwind.tokens.js'), tailwind); + } + + console.log(`✅ Generated design tokens in ${formats.join(', ')} format(s)`); +} + +function generateCSSVariables(tokens: any): string { + let css = ':root {\n'; + + // Colors + css += ' /* Colors */\n'; + tokens.colors.forEach((token: any) => { + css += ` --color-${token.name.toLowerCase().replace(/\s+/g, '-')}: ${token.value};\n`; + }); + + // Typography + css += '\n /* Typography */\n'; + tokens.typography.forEach((token: any) => { + const name = token.name.toLowerCase().replace(/\s+/g, '-'); + css += ` --font-family-${name}: ${token.fontFamily};\n`; + css += ` --font-size-${name}: ${token.fontSize}px;\n`; + css += ` --font-weight-${name}: ${token.fontWeight};\n`; + css += ` --line-height-${name}: ${token.lineHeight};\n`; + }); + + // Spacing + css += '\n /* Spacing */\n'; + tokens.spacing.forEach((token: any) => { + css += ` --spacing-${token.name}: ${token.value}px;\n`; + }); + + // Shadows + css += '\n /* Shadows */\n'; + tokens.shadows.forEach((token: any) => { + css += ` --shadow-${token.name.toLowerCase().replace(/\s+/g, '-')}: ${token.value};\n`; + }); + + css += '}\n'; + + return css; +} + +function generateJavaScript(tokens: any): string { + const js = ` +export const colors = { +${tokens.colors.map((t: any) => ` '${t.name}': '${t.value}',`).join('\n')} +}; + +export const typography = { +${tokens.typography.map((t: any) => ` '${t.name}': { + fontFamily: '${t.fontFamily}', + fontSize: ${t.fontSize}, + fontWeight: ${t.fontWeight}, + lineHeight: ${t.lineHeight}, + },`).join('\n')} +}; + +export const spacing = { +${tokens.spacing.map((t: any) => ` '${t.name}': ${t.value},`).join('\n')} +}; + +export const shadows = { +${tokens.shadows.map((t: any) => ` '${t.name}': '${t.value}',`).join('\n')} +}; +`; + + return js.trim(); +} + +// Usage +generateTokens({ + fileKey: 'ABC123XYZ456', + outputDir: './src/design-tokens', + formats: ['css', 'ts', 'json', 'tailwind'], +}).catch(console.error); +``` + +### 6. Token Synchronization (Watch Mode) + +**Auto-sync on Figma Changes**: + +```typescript +import { watch } from 'fs'; + +async function watchFigmaTokens(fileKey: string, outputDir: string, pollInterval = 60000) { + let lastVersion: string | null = null; + + setInterval(async () => { + const importer = new FigmaImporter({ + accessToken: process.env.FIGMA_ACCESS_TOKEN!, + fileKey, + }); + + const file = await importer.fetchFile(); + + if (lastVersion && file.version !== lastVersion) { + console.log(`🔄 Figma file updated (v${file.version}). Re-generating tokens...`); + + await generateTokens({ + fileKey, + outputDir, + formats: ['css', 'ts', 'json'], + }); + + console.log('✅ Tokens synchronized!'); + } + + lastVersion = file.version; + }, pollInterval); + + console.log(`👀 Watching Figma file for changes (polling every ${pollInterval / 1000}s)...`); +} + +// Usage +watchFigmaTokens('ABC123XYZ456', './src/design-tokens', 60000); // Check every minute +``` + +### 7. Theme Support (Light/Dark Mode) + +**Generate Theme Tokens**: + +```typescript +// tokens/themes/light.ts +export const lightTheme = { + colors: { + background: '#FFFFFF', + foreground: '#111827', + primary: '#0066FF', + secondary: '#6B7280', + border: '#E5E7EB', + }, +}; + +// tokens/themes/dark.ts +export const darkTheme = { + colors: { + background: '#111827', + foreground: '#F9FAFB', + primary: '#3B82F6', + secondary: '#9CA3AF', + border: '#374151', + }, +}; + +// CSS Variables with theme support +:root { + --color-background: #FFFFFF; + --color-foreground: #111827; +} + +[data-theme="dark"] { + --color-background: #111827; + --color-foreground: #F9FAFB; +} +``` + +## Workflow + +1. Ask about Figma file and token extraction preferences +2. Fetch color, typography, spacing, and shadow styles from Figma +3. Analyze and categorize tokens (brand, semantic, primitives) +4. Ask about output formats (CSS, SCSS, JS, JSON, Tailwind) +5. Generate hierarchical token structure +6. Create token files in requested formats +7. Set up theme support if needed (light/dark modes) +8. Provide integration examples for each format +9. Optionally set up watch mode for auto-sync +10. Generate documentation with token usage examples + +## When to Use + +- Setting up design systems from Figma +- Synchronizing design tokens across platforms (web, iOS, Android) +- Migrating from hardcoded values to token-based theming +- Implementing light/dark mode support +- Automating design-to-code token updates +- Standardizing design values across teams + +## Best Practices + +1. **Organization**: Use hierarchical structure (global → semantic → component) +2. **Naming**: Follow consistent naming conventions (BEM or similar) +3. **Formats**: Generate multiple formats for different use cases +4. **Aliasing**: Use token references for semantic tokens (W3C format) +5. **Documentation**: Include descriptions from Figma styles +6. **Versioning**: Track Figma file versions in metadata +7. **Automation**: Set up CI/CD for token synchronization +8. **Validation**: Validate token values before deployment + +Extract and sync design tokens with production-ready automation! diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..0d1f85d --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,53 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:anton-abyzov/specweave:plugins/specweave-figma", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "33f52907f3e3b3677c987b18109af9d6f009508a", + "treeHash": "0b23a6332a9470a34badbc0230a31c22f8a0d6cf91e2073c5e76ea36c473399c", + "generatedAt": "2025-11-28T10:13:53.959507Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "specweave-figma", + "description": "Comprehensive Figma design-to-code automation. Import Figma designs via API/MCP, convert components to React/Vue/Angular, extract design tokens, generate responsive layouts, and sync design systems. Focus on accelerating frontend development from Figma designs.", + "version": "0.24.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "775af1f0650b658a8accf4709e2858d66381f48aba95042cb3bf4d33047afe25" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "00ce31a0baaeadcd486135497a0041eac895ebfef65a119d58bb52cca639f620" + }, + { + "path": "commands/figma-tokens.md", + "sha256": "27cbd46bc61a8d488e9a4a535c97cdf922330cd617af6a1591983731e26a8628" + }, + { + "path": "commands/figma-to-react.md", + "sha256": "c0f6981974f337d7ecbacf6f15fa09cb2c36abe53751ea3e3499585635be1e28" + }, + { + "path": "commands/figma-import.md", + "sha256": "df755bf084838a9993c1d06e0cece9ea7116ca652ffb46cbae24d64a8c7ecc3b" + } + ], + "dirSha256": "0b23a6332a9470a34badbc0230a31c22f8a0d6cf91e2073c5e76ea36c473399c" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file