mcpField
F
Figma Developer

Figma Developer

github.com/daffy0208设计与 UI38149 次安装44 周前更新
代码仓库

关于 Figma Developer

Figma Developer 是一项介绍将 Figma 设计转为代码的工作流技能,涵盖提取设计令牌和组件结构、生成组件,以及自动化同步流程。

主要功能

  1. 01使用访问令牌连接 Figma
  2. 02提取设计令牌
  3. 03将令牌导出为代码
  4. 04提取并生成组件
  5. 05自动化设计同步工作流

适用场景

  1. 01将 Figma 设计转为代码令牌
  2. 02根据 Figma 文件生成界面组件
  3. 03同步设计令牌和组件

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/daffy0208/ai-dev-standards --skill figma-developer

安装后在 Claude Code 中输入/figma-developer确认已加载。

附带文件manifest.yaml

详细介绍

Figma 开发者

将 Figma 设计转化为可用于生产环境的代码。

核心原则

设计是唯一可信来源。

设计师在 Figma 中工作。开发者基于 Figma 构建。两者之间的衔接应该自动完成,而非手动处理。


阶段 1:设置与身份验证

获取 Figma 访问令牌
  1. 前往 Figma 设置
  2. 向下滚动到“个人访问令牌”
  3. 点击“生成新令牌”
  4. 为令牌命名(例如“开发”)
  5. 复制并安全保存
环境设置
# .env
FIGMA_ACCESS_TOKEN=figd_...
安装 Figma 客户端
npm install node-fetch
测试连接
import { FigmaClient } from '@/integrations/design-tools/figma/client'

const client = new FigmaClient({
  accessToken: process.env.FIGMA_ACCESS_TOKEN
})

// Test with a public file
const file = await client.getFile('abc123xyz')
console.log('Connected! File:', file.name)

阶段 2:提取设计令牌

什么是设计令牌?

设计令牌是以代码形式存储的设计决策(颜色、字体排版、间距)。

优点:

  • 单一可信来源
  • 在不同平台上保持一致
  • 易于更新
  • 类型安全
从 Figma 提取令牌
// scripts/sync-design-tokens.ts

import { FigmaClient } from '@/integrations/design-tools/figma/client'
import fs from 'fs/promises'

async function syncDesignTokens() {
  const client = new FigmaClient()
  const fileKey = 'YOUR_FIGMA_FILE_KEY'

  console.log('Extracting design tokens...')

  // Extract tokens
  const tokens = await client.extractDesignTokens(fileKey)

  console.log(`Found:`)
  console.log(`  ${tokens.colors.length} colors`)
  console.log(`  ${tokens.typography.length} text styles`)
  console.log(`  ${tokens.spacing.length} spacing values`)

  // Export as CSS
  const css = await client.exportTokensAsCSS(fileKey)
  await fs.writeFile('src/styles/design-tokens.css', css)

  // Export as JSON
  const json = await client.exportTokensAsJSON(fileKey)
  await fs.writeFile('src/styles/design-tokens.json', json)

  console.log('Design tokens synced!')
}

syncDesignTokens()
在代码中使用令牌
// src/styles/design-tokens.css
:root {
  /* Colors */
  --color-primary: #0066cc;
  --color-secondary: #10b981;
  --color-neutral-100: #f9fafb;
  --color-neutral-900: #111827;

  /* Typography */
  --font-heading-family: Inter;
  --font-heading-size: 48px;
  --font-heading-weight: 700;

  /* Spacing */
  --space-4: 16px;
  --space-8: 32px;
}

在 React 中使用:

// components/Button.tsx

export function Button({ children }: { children: React.ReactNode }) {
  return (
    <button
      style={{
        backgroundColor: 'var(--color-primary)',
        color: 'white',
        padding: 'var(--space-4)',
        fontFamily: 'var(--font-heading-family)',
        fontWeight: 'var(--font-heading-weight)',
        border: 'none',
        borderRadius: '8px',
        cursor: 'pointer'
      }}
    >
      {children}
    </button>
  )
}

阶段 3:导出资源

将图标导出为 SVG
// scripts/export-icons.ts

import { FigmaClient } from '@/integrations/design-tools/figma/client'
import fs from 'fs/promises'

async function exportIcons() {
  const client = new FigmaClient()
  const fileKey = 'YOUR_FIGMA_FILE_KEY'

  // Get file
  const file = await client.getFile(fileKey)

  // Find "Icons" frame
  const iconsFrame = findNode(file.document, 'Icons')

  if (!iconsFrame || !iconsFrame.children) {
    throw new Error('Icons frame not found')
  }

  console.log(`Found ${iconsFrame.children.length} icons`)

  // Export as SVG
  const iconIds = iconsFrame.children.map(child => child.id)
  const svgs = await client.exportImages(fileKey, iconIds, {
    format: 'svg'
  })

  // Save each SVG
  for (const svg of svgs) {
    const response = await fetch(svg.url)
    const content = await response.text()
    await fs.writeFile(`public/icons/${svg.name}.svg`, content)
    console.log(`  ✓ ${svg.name}.svg`)
  }

  console.log('Icons exported!')
}

function findNode(node: any, name: string): any {
  if (node.name === name) return node
  if (node.children) {
    for (const child of node.children) {
      const found = findNode(child, name)
      if (found) return found
    }
  }
  return null
}

exportIcons()
生成 React 图标组件
// scripts/generate-icon-components.ts

import { FigmaClient } from '@/integrations/design-tools/figma/client'
import fs from 'fs/promises'

async function generateIconComponents() {
  const client = new FigmaClient()
  const fileKey = 'YOUR_FIGMA_FILE_KEY'

  const file = await client.getFile(fileKey)
  const iconsFrame = findNode(file.document, 'Icons')

  if (!iconsFrame || !iconsFrame.children) {
    throw new Error('Icons frame not found')
  }

  // Export icons
  const iconIds = iconsFrame.children.map(child => child.id)
  const svgs = await client.exportImages(fileKey, iconIds, {
    format: 'svg'
  })

  // Generate React components
  for (const svg of svgs) {
    const response = await fetch(svg.url)
    const svgContent = await response.text()

    // Convert to React component
    const componentName = toPascalCase(svg.name)
    const component = `
import React from 'react'

export function ${componentName}Icon(props: React.SVGProps<SVGSVGElement>) {
  return (
    ${svgContent.replace('<svg', '<svg {...props}')}
  )
}
    `.trim()

    await fs.writeFile(`components/icons/${componentName}Icon.tsx`, component)
    console.log(`  ✓ ${componentName}Icon.tsx`)
  }

  // Generate index file
  const indexContent = svgs
    .map(svg => {
      const componentName = toPascalCase(svg.name)
      return `export { ${componentName}Icon } from './${componentName}Icon'`
    })
    .join('\n')

  await fs.writeFile('components/icons/index.ts', indexContent)

  console.log('Icon components generated!')
}

function toPascalCase(str: string): string {
  return str
    .split(/[-_\s]+/)
    .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
    .join('')
}

function findNode(node: any, name: string): any {
  if (node.name === name) return node
  if (node.children) {
    for (const child of node.children) {
      const found = findNode(child, name)
      if (found) return found
    }
  }
  return null
}

generateIconComponents()

用法:

import { HomeIcon, UserIcon, SettingsIcon } from '@/components/icons'

export function Navigation() {
  return (
    <nav>
      <HomeIcon width={24} height={24} />
      <UserIcon width={24} height={24} />
      <SettingsIcon width={24} height={24} />
    </nav>
  )
}

阶段 4:组件生成

提取组件结构
// scripts/extract-components.ts

import { FigmaClient } from '@/integrations/design-tools/figma/client'

async function extractComponents() {
  const client = new FigmaClient()
  const fileKey = 'YOUR_FIGMA_FILE_KEY'

  // Get components
  const components = await client.getFileComponents(fileKey)

  console.log('Components:')
  for (const [key, component] of Object.entries(components)) {
    console.log(`  ${component.name}`)
    console.log(`    Key: ${component.key}`)
    console.log(`    Description: ${component.description}`)
  }

  // Get component sets (variants)
  const componentSets = await client.getComponentSets(fileKey)

  console.log('\nComponent Sets:')
  for (const [setId, variants] of Object.entries(componentSets)) {
    console.log(`  Set: ${setId}`)
    for (const variant of variants) {
      console.log(`    - ${variant.name}`)
    }
  }
}

extractComponents()
根据 Figma 生成按钮组件
// scripts/generate-button.ts

import { FigmaClient } from '@/integrations/design-tools/figma/client'
import fs from 'fs/promises'

async function generateButtonComponent() {
  const client = new FigmaClient()
  const fileKey = 'YOUR_FIGMA_FILE_KEY'

  // Get button component
  const components = await client.getFileComponents(fileKey)
  const buttonComponent = Object.values(components).find(c =>
    c.name.toLowerCase().includes('button')
  )

  if (!buttonComponent) {
    throw new Error('Button component not found')
  }

  // Get component node
  const file = await client.getFile(fileKey)
  const buttonNode = findNodeById(file.document, buttonComponent.key)

  if (!buttonNode) {
    throw new Error('Button node not found')
  }

  // Extract styles
  const styles = extractStyles(buttonNode)

  // Generate React component
  const component = `
import React from 'react'

interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'ghost'
  size?: 'sm' | 'md' | 'lg'
  children: React.ReactNode
  onClick?: () => void
}

export function Button({
  variant = 'primary',
  size = 'md',
  children,
  onClick
}: ButtonProps) {
  const baseStyles = {
    fontFamily: '${styles.fontFamily}',
    fontWeight: ${styles.fontWeight},
    fontSize: '${styles.fontSize}px',
    padding: '${styles.padding}',
    borderRadius: '${styles.borderRadius}px',
    border: 'none',
    cursor: 'pointer',
    transition: 'all 0.2s'
  }

  const variantStyles = {
    primary: {
      backgroundColor: '${styles.backgroundColor}',
      color: '${styles.color}'
    },
    secondary: {
      backgroundColor: 'transparent',
      color: '${styles.backgroundColor}',
      border: '2px solid ${styles.backgroundColor}'
    },
    ghost: {
      backgroundColor: 'transparent',
      color: '${styles.color}'
    }
  }

  return (
    <button
      style={{ ...baseStyles, ...variantStyles[variant] }}
      onClick={onClick}
    >
      {children}
    </button>
  )
}
  `.trim()

  await fs.writeFile('components/Button.tsx', component)
  console.log('Button component generated!')
}

function findNodeById(node: any, id: string): any {
  if (node.id === id) return node
  if (node.children) {
    for (const child of node.children) {
      const found = findNodeById(child, id)
      if (found) return found
    }
  }
  return null
}

function extractStyles(node: any) {
  return {
    fontFamily: node.style?.fontFamily || 'Inter',
    fontWeight: node.style?.fontWeight || 600,
    fontSize: node.style?.fontSize || 16,
    padding: '12px 24px',
    borderRadius: node.cornerRadius || 8,
    backgroundColor: rgbToHex(node.fills?.[0]?.color || { r: 0, g: 0.4, b: 0.8 }),
    color: '#ffffff'
  }
}

function rgbToHex(color: any): string {
  const r = Math.round(color.r * 255)
    .toString(16)
    .padStart(2, '0')
  const g = Math.round(color.g * 255)
    .toString(16)
    .padStart(2, '0')
  const b = Math.round(color.b * 255)
    .toString(16)
    .padStart(2, '0')
  return `#${r}${g}${b}`
}

generateButtonComponent()

阶段 5:自动化工作流

设置 GitHub Actions
# .github/workflows/sync-figma.yml

name: Sync Figma Design Tokens

on:
  schedule:
    - cron: '0 9 * * *' # Every day at 9am
  workflow_dispatch: # Manual trigger

jobs:
  sync:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - uses: actions/setup-node@v3
        with:
          node-version: 18

      - run: npm install

      - name: Sync design tokens
        env:
          FIGMA_ACCESS_TOKEN: ${{ secrets.FIGMA_ACCESS_TOKEN }}
        run: npm run sync:design-tokens

      - name: Create Pull Request
        uses: peter-evans/create-pull-request@v5
        with:
          title: 'chore: sync design tokens from Figma'
          body: 'Automated sync of design tokens from Figma'
          branch: 'figma/sync-tokens'
          commit-message: 'chore: sync design tokens'
Package.json 脚本
{
  "scripts": {
    "sync:design-tokens": "tsx scripts/sync-design-tokens.ts",
    "export:icons": "tsx scripts/export-icons.ts",
    "generate:icons": "tsx scripts/generate-icon-components.ts",
    "figma:sync-all": "npm run sync:design-tokens && npm run generate:icons"
  }
}

最佳实践

1. 整理 Figma 文件

结构:

Design System File
├── 📄 Cover (description)
├── 🎨 Colors (all color styles)
├── 📝 Typography (all text styles)
├── 📏 Spacing (spacing guide)
├── 🧩 Components
│   ├── Buttons
│   ├── Forms
│   └── Cards
└── 🖼️ Icons (all icons in one frame)
2. 命名规范

颜色:

Primary/500
Secondary/500
Neutral/100
Neutral/900
Success
Error

字体排版:

Heading/Large
Heading/Medium
Body/Regular
Body/Small

组件:

Button/Primary
Button/Secondary
Card/Default
Card/Elevated
3. 使用 Figma 变量(Beta)

Figma 现已原生支持变量。可将它们用于:

  • 颜色
  • 间距
  • 边框圆角
  • 字体大小

通过 API 自动提取这些变量。

4. 版本控制
// Check for Figma updates
async function checkForUpdates() {
  const client = new FigmaClient()
  const fileKey = 'YOUR_FIGMA_FILE_KEY'

  const file = await client.getFile(fileKey)
  const currentVersion = file.version

  // Store in database or file
  const previousVersion = await getPreviousVersion()

  if (currentVersion !== previousVersion) {
    console.log('Figma file updated!')
    console.log(`Version: ${previousVersion} → ${currentVersion}`)

    // Trigger sync
    await syncDesignTokens()
    await savePreviousVersion(currentVersion)
  } else {
    console.log('No updates')
  }
}

常见模式

模式 1:基于令牌的开发
// 1. Extract tokens
const tokens = await client.extractDesignTokens(fileKey)

// 2. Generate CSS variables
const css = generateCSS(tokens)

// 3. Generate TypeScript types
const types = `
export type ColorToken =
  ${tokens.colors.map(c => `| '${c.name}'`).join('\n  ')}

export type SpacingToken =
  ${tokens.spacing.map(s => `| '${s.name}'`).join('\n  ')}
`.trim()

await fs.writeFile('src/types/tokens.ts', types)

// 4. Use in components
import { ColorToken } from '@/types/tokens'

interface ButtonProps {
  color: ColorToken
}
模式 2:组件同步
// Keep components in sync with Figma
async function syncComponent(componentName: string) {
  const client = new FigmaClient()
  const fileKey = 'YOUR_FIGMA_FILE_KEY'

  // Get component from Figma
  const components = await client.getFileComponents(fileKey)
  const component = Object.values(components).find(c => c.name === componentName)

  if (!component) {
    throw new Error(`Component not found: ${componentName}`)
  }

  // Generate code
  const code = await generateComponentCode(component)

  // Write to file
  await fs.writeFile(`components/${componentName}.tsx`, code)

  console.log(`Synced: ${componentName}`)
}

故障排除

问题:令牌名称不匹配

问题: Figma 样式名称包含空格或特殊字符

解决方案: 规范化名称

function normalizeTokenName(name: string): string {
  return name
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-|-$/g, '')
}
问题:颜色看起来不同

问题: RGB 值需要转换

解决方案: 使用正确的色彩空间转换

function rgbToHex(color: { r: number; g: number; b: number }): string {
  const r = Math.round(color.r * 255)
  const g = Math.round(color.g * 255)
  const b = Math.round(color.b * 255)
  return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
}
问题:API 速率限制

问题: 请求过多

解决方案: 缓存响应

const cache = new Map<string, { data: any; timestamp: number }>()

async function getCachedFile(fileKey: string) {
  const cached = cache.get(fileKey)

  if (cached && Date.now() - cached.timestamp < 60000) {
    return cached.data
  }

  const file = await client.getFile(fileKey)
  cache.set(fileKey, { data: file, timestamp: Date.now() })
  return file
}

工具与资源

Figma 插件:

  • Figma Tokens - 管理设计令牌
  • Design Tokens - 导出令牌
  • Figma to Code - 生成代码

库:

  • @figma/rest-api-spec - TypeScript 类型
  • figma-api - 备用客户端
  • style-dictionary - 转换令牌

相关技能:

  • design-system-architect - 构建设计系统
  • visual-designer - 设计原则
  • asset-manager - 管理导出的资源

自动将设计转化为代码。 🎨→💻

评论

支持 Markdown