Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 17:56:33 +08:00
commit 91dfd0eac2
6 changed files with 2407 additions and 0 deletions

690
commands/figma-import.md Normal file
View File

@@ -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<string, string>;
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!

834
commands/figma-to-react.md Normal file
View File

@@ -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<string, ComponentProperty>;
}
```
**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 (
<StyledContainer
variant={variant}
size={size}
disabled={disabled}
onClick={onClick}
>
{children}
</StyledContainer>
);
};
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<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'outline';
size?: 'sm' | 'md' | 'lg';
fullWidth?: boolean;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
loading?: boolean;
}
const Button: FC<ButtonProps> = ({
variant = 'primary',
size = 'md',
fullWidth = false,
leftIcon,
rightIcon,
loading = false,
disabled,
children,
...props
}) => {
return (
<StyledButton
variant={variant}
size={size}
fullWidth={fullWidth}
disabled={disabled || loading}
{...props}
>
{leftIcon && <IconWrapper>{leftIcon}</IconWrapper>}
{loading ? <Spinner /> : children}
{rightIcon && <IconWrapper>{rightIcon}</IconWrapper>}
</StyledButton>
);
};
const StyledButton = styled.button<ButtonProps>`
/* 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<ChipProps> = ({
state = 'default',
size = 'md',
iconPosition = 'none',
label,
icon,
onClick,
}) => {
return (
<StyledChip
state={state}
size={size}
onClick={state !== 'disabled' ? onClick : undefined}
>
{iconPosition === 'left' && icon && <IconWrapper>{icon}</IconWrapper>}
<Label>{label}</Label>
{iconPosition === 'right' && icon && <IconWrapper>{icon}</IconWrapper>}
</StyledChip>
);
};
const StyledChip = styled.div<ChipProps>`
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<ButtonProps> = ({
children,
disabled,
loading,
ariaLabel,
...props
}) => {
return (
<button
aria-label={ariaLabel || (typeof children === 'string' ? children : undefined)}
aria-disabled={disabled || loading}
aria-busy={loading}
disabled={disabled || loading}
{...props}
>
{children}
</button>
);
};
// Icon buttons MUST have aria-label
const IconButton: FC<IconButtonProps> = ({ icon, onClick, ariaLabel }) => {
return (
<button
aria-label={ariaLabel} // Required for screen readers
onClick={onClick}
>
{icon}
</button>
);
};
```
**Semantic HTML**:
```typescript
// ❌ Wrong: div soup
<div onClick={onClick}>Submit</div>
// ✅ Correct: semantic button
<button onClick={onClick}>Submit</button>
// ❌ Wrong: generic div
<div>Card Title</div>
// ✅ Correct: heading
<h2>Card Title</h2>
```
### 8. Storybook Integration
**Generate Storybook Stories**:
```typescript
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import Button from './Button';
const meta: Meta<typeof Button> = {
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<typeof Button>;
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: <IconArrowLeft />,
rightIcon: <IconArrowRight />,
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(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('calls onClick when clicked', () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByText('Click me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('does not call onClick when disabled', () => {
const handleClick = vi.fn();
render(<Button disabled onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByText('Click me'));
expect(handleClick).not.toHaveBeenCalled();
});
it('renders loading state correctly', () => {
render(<Button loading>Click me</Button>);
expect(screen.queryByText('Click me')).not.toBeInTheDocument();
});
it('renders icons correctly', () => {
render(
<Button leftIcon={<span data-testid="left-icon"></span>}>
Click me
</Button>
);
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!

815
commands/figma-tokens.md Normal file
View File

@@ -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<ColorToken[]> {
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<TypographyToken[]> {
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<SpacingToken[]> {
const importer = new FigmaImporter({
accessToken: process.env.FIGMA_ACCESS_TOKEN!,
fileKey,
});
const file = await importer.fetchFile();
const spacingValues = new Set<number>();
// 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<ShadowToken[]> {
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!