🎯 Рекомендуемые коллекции

Балансированные коллекции примеров кода из различных категорий, которые вы можете исследовать

Примеры Figma Plugins

Примеры разработки плагинов Figma с созданием UI, автоматизацией дизайна и инструментами улучшения рабочих процессов

💻 Плагин Figma Hello World typescript

🟢 simple ⭐⭐

Базовая настройка плагина Figma с созданием UI, взаимодействием API и основами передачи сообщений

⏱️ 20 min 🏷️ figma, plugin, typescript, design tools
Prerequisites: TypeScript basics, Figma API knowledge, HTML/CSS
// Figma Plugin Hello World
// Basic plugin structure with UI and API interaction

// Manifest file (manifest.json):
/*
{
  "name": "Hello World Plugin",
  "id": "YOUR_UNIQUE_PLUGIN_ID",
  "api": "1.0.0",
  "main": "dist/code.js",
  "ui": "dist/ui.html",
  "editorType": ["figma"]
}
*/

// code.ts - Main plugin logic
import { showUI } from '@create-figma-plugin/utilities'

export default function () {
  figma.currentPage.selection = figma.currentPage.selection

  // Create initial UI state
  const initialState = {
    count: 0,
    message: 'Hello from Figma Plugin!',
    selectedNodes: figma.currentPage.selection.length,
    user: figma.currentUser?.name || 'Anonymous'
  }

  showUI({ height: 400, width: 320 }, initialState)
}

// Message handling from UI
figma.ui.onmessage = async (msg: any) => {
  switch (msg.type) {
    case 'create-shape':
      await createShape(msg.shapeType, msg.properties)
      break

    case 'get-selection':
      const selection = figma.currentPage.selection.map(node => ({
        id: node.id,
        name: node.name,
        type: node.type
      }))
      figma.ui.postMessage({
        type: 'selection-updated',
        selection
      })
      break

    case 'update-selection':
      if (msg.nodeIds && msg.nodeIds.length > 0) {
        const nodes = []
        for (const id of msg.nodeIds) {
          const node = figma.getNodeById(id)
          if (node) nodes.push(node)
        }
        figma.currentPage.selection = nodes
        figma.viewport.scrollAndZoomIntoView(nodes)
      }
      break

    case 'notify':
      figma.notify(msg.message)
      break

    case 'close-plugin':
      figma.closePlugin()
      break

    case 'export-selection':
      await exportSelection(msg.format)
      break

    case 'apply-style':
      await applyStyleToSelection(msg.style)
      break

    default:
      console.log('Unknown message type:', msg.type)
  }
}

// Shape creation functions
async function createShape(shapeType: string, properties: any) {
  let node: SceneNode

  switch (shapeType) {
    case 'rectangle':
      node = figma.createRectangle()
      node.resize(properties.width || 100, properties.height || 100)
      break

    case 'circle':
      node = figma.createEllipse()
      node.resize(properties.width || 100, properties.height || 100)
      break

    case 'star':
      node = figma.createStar()
      ;(node as StarNode).pointCount = properties.points || 5
      break

    case 'polygon':
      node = figma.createPolygon()
      ;(node as PolygonNode).pointCount = properties.sides || 6
      break

    case 'text':
      node = figma.createText()
      await figma.loadFontAsync({ family: 'Inter', style: 'Regular' })
      ;(node as TextNode).characters = properties.text || 'Hello World'
      ;(node as TextNode).fontSize = properties.fontSize || 24
      break

    default:
      node = figma.createRectangle()
      node.resize(100, 100)
  }

  // Apply common properties
  if (properties.x !== undefined && properties.y !== undefined) {
    node.x = properties.x
    node.y = properties.y
  } else {
    // Center in viewport
    const viewport = figma.viewport.center
    node.x = viewport.x - (node.width / 2)
    node.y = viewport.y - (node.height / 2)
  }

  if (properties.fill) {
    if ('fills' in node) {
      node.fills = [{
        type: 'SOLID',
        color: {
          r: properties.fill.r || 0.5,
          g: properties.fill.g || 0.5,
          b: properties.fill.b || 1.0
        }
      }]
    }
  }

  if (properties.name) {
    node.name = properties.name
  }

  // Add to page
  figma.currentPage.appendChild(node)
  figma.currentPage.selection = [node]

  // Notify UI
  figma.ui.postMessage({
    type: 'shape-created',
    shape: {
      id: node.id,
      name: node.name,
      type: node.type,
      width: node.width,
      height: node.height
    }
  })
}

// Export functionality
async function exportSelection(format: string) {
  if (figma.currentPage.selection.length === 0) {
    figma.notify('Please select something to export')
    return
  }

  try {
    switch (format) {
      case 'svg':
        await exportAsSVG()
        break
      case 'png':
        await exportAsPNG()
        break
      case 'json':
        await exportAsJSON()
        break
      default:
        figma.notify('Unsupported export format')
    }
  } catch (error) {
    figma.notify(`Export failed: ${error.message}`)
  }
}

async function exportAsSVG() {
  const nodes = figma.currentPage.selection
  for (const node of nodes) {
    if ('exportAsync' in node) {
      const bytes = await node.exportAsync({
        format: 'SVG',
        contentsOnly: true
      })
      figma.ui.postMessage({
        type: 'export-complete',
        data: Array.from(bytes),
        filename: `${node.name}.svg`
      })
    }
  }
}

async function exportAsPNG() {
  const nodes = figma.currentPage.selection
  for (const node of nodes) {
    if ('exportAsync' in node) {
      const bytes = await node.exportAsync({
        format: 'PNG',
        constraint: { type: 'SCALE', value: 2 }
      })
      figma.ui.postMessage({
        type: 'export-complete',
        data: Array.from(bytes),
        filename: `${node.name}.png`
      })
    }
  }
}

async function exportAsJSON() {
  const selection = figma.currentPage.selection
  const jsonData = JSON.stringify({
    nodes: selection.map(node => ({
      id: node.id,
      name: node.name,
      type: node.type,
      x: node.x,
      y: node.y,
      width: node.width,
      height: node.height,
      visible: node.visible,
      locked: node.locked
    })),
    page: {
      name: figma.currentPage.name,
      selectionCount: selection.length
    }
  }, null, 2)

  figma.ui.postMessage({
    type: 'export-complete',
    data: jsonData,
    filename: 'selection.json'
  })
}

// Style application
async function applyStyleToSelection(style: any) {
  const selection = figma.currentPage.selection

  for (const node of selection) {
    if ('fills' in node && style.fills) {
      node.fills = style.fills
    }
    if ('strokes' in node && style.strokes) {
      node.strokes = style.strokes
    }
    if ('cornerRadius' in node && style.cornerRadius !== undefined) {
      node.cornerRadius = style.cornerRadius
    }
    if ('effects' in node && style.effects) {
      node.effects = style.effects
    }
  }

  figma.notify(`Applied style to ${selection.length} nodes`)
}

// ui.html - Plugin interface
/*
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Hello World Plugin</title>
  <style>
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      margin: 0;
      padding: 16px;
      background: #f8f9fa;
    }

    .header {
      text-align: center;
      margin-bottom: 20px;
    }

    .header h2 {
      margin: 0;
      color: #333;
      font-size: 18px;
    }

    .user-info {
      color: #666;
      font-size: 12px;
      margin-top: 4px;
    }

    .section {
      background: white;
      border-radius: 8px;
      padding: 16px;
      margin-bottom: 12px;
      box-shadow: 0 1px 3px rgba(0,0,0,0.1);
    }

    .section-title {
      font-weight: 600;
      margin-bottom: 12px;
      color: #333;
      font-size: 14px;
    }

    .button-group {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 8px;
      margin-bottom: 12px;
    }

    button {
      padding: 8px 12px;
      border: none;
      border-radius: 6px;
      background: #0d99ff;
      color: white;
      cursor: pointer;
      font-size: 12px;
      transition: background 0.2s;
    }

    button:hover {
      background: #0d7dd8;
    }

    button:active {
      transform: translateY(1px);
    }

    button.secondary {
      background: #e4e6ea;
      color: #333;
    }

    button.secondary:hover {
      background: #d7d9dd;
    }

    button.danger {
      background: #ff4d4d;
    }

    button.danger:hover {
      background: #ff3333;
    }

    .input-group {
      margin-bottom: 12px;
    }

    label {
      display: block;
      margin-bottom: 4px;
      font-size: 12px;
      color: #666;
    }

    input, select, textarea {
      width: 100%;
      padding: 8px;
      border: 1px solid #ddd;
      border-radius: 4px;
      font-size: 12px;
      box-sizing: border-box;
    }

    .status {
      background: #e8f4fd;
      padding: 12px;
      border-radius: 6px;
      font-size: 12px;
      color: #0d99ff;
      text-align: center;
    }

    .selection-list {
      max-height: 120px;
      overflow-y: auto;
      border: 1px solid #ddd;
      border-radius: 4px;
      padding: 8px;
    }

    .selection-item {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 4px 0;
      border-bottom: 1px solid #eee;
      font-size: 12px;
    }

    .selection-item:last-child {
      border-bottom: none;
    }

    .node-type {
      background: #0d99ff;
      color: white;
      padding: 2px 6px;
      border-radius: 3px;
      font-size: 10px;
    }

    .color-picker {
      display: flex;
      gap: 8px;
      flex-wrap: wrap;
    }

    .color-option {
      width: 32px;
      height: 32px;
      border-radius: 4px;
      cursor: pointer;
      border: 2px solid transparent;
    }

    .color-option:hover {
      border-color: #0d99ff;
    }

    .color-option.selected {
      border-color: #0d99ff;
      box-shadow: 0 0 0 2px rgba(13, 153, 255, 0.2);
    }
  </style>
</head>
<body>
  <div class="header">
    <h2>Hello World Plugin</h2>
    <div class="user-info" id="userInfo">Loading...</div>
  </div>

  <div class="status" id="status">
    Ready to create shapes!
  </div>

  <div class="section">
    <div class="section-title">Create Shapes</div>
    <div class="button-group">
      <button onclick="createShape('rectangle')">Rectangle</button>
      <button onclick="createShape('circle')">Circle</button>
      <button onclick="createShape('star')">Star</button>
      <button onclick="createShape('text')">Text</button>
    </div>

    <div class="input-group">
      <label>Shape Name</label>
      <input type="text" id="shapeName" placeholder="Enter shape name">
    </div>

    <div class="input-group">
      <label>Color</label>
      <div class="color-picker" id="colorPicker">
        <!-- Colors will be added by JavaScript -->
      </div>
    </div>
  </div>

  <div class="section">
    <div class="section-title">Selection</div>
    <div class="button-group">
      <button onclick="getSelection()">Refresh</button>
      <button class="secondary" onclick="clearSelection()">Clear</button>
    </div>
    <div class="selection-list" id="selectionList">
      <div style="color: #999; text-align: center;">No selection</div>
    </div>
  </div>

  <div class="section">
    <div class="section-title">Export</div>
    <div class="button-group">
      <button onclick="exportSelection('svg')">SVG</button>
      <button onclick="exportSelection('png')">PNG</button>
      <button onclick="exportSelection('json')">JSON</button>
      <button class="secondary" onclick="exportSelection('jpg')">JPG</button>
    </div>
  </div>

  <div class="section">
    <div class="section-title">Quick Actions</div>
    <div class="button-group">
      <button onclick="duplicateSelection()">Duplicate</button>
      <button onclick="groupSelection()">Group</button>
      <button onclick="alignSelection()">Align</button>
      <button class="danger" onclick="deleteSelection()">Delete</button>
    </div>
  </div>

  <script>
    let selectedColor = { r: 0.5, g: 0.5, b: 1.0 };

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      initializeColorPicker();
      setInterval(getSelection, 1000); // Auto-refresh selection
    });

    // Initialize color picker
    function initializeColorPicker() {
      const colors = [
        { r: 1.0, g: 0.3, b: 0.3 }, // Red
        { r: 0.3, g: 1.0, b: 0.3 }, // Green
        { r: 0.3, g: 0.3, b: 1.0 }, // Blue
        { r: 1.0, g: 1.0, b: 0.3 }, // Yellow
        { r: 1.0, g: 0.3, b: 1.0 }, // Magenta
        { r: 0.3, g: 1.0, b: 1.0 }, // Cyan
        { r: 1.0, g: 0.5, b: 0.0 }, // Orange
        { r: 0.5, g: 0.5, b: 1.0 }, // Default blue
      ];

      const picker = document.getElementById('colorPicker');
      colors.forEach((color, index) => {
        const colorDiv = document.createElement('div');
        colorDiv.className = 'color-option' + (index === 7 ? ' selected' : '');
        colorDiv.style.backgroundColor = `rgb(${Math.round(color.r * 255)}, ${Math.round(color.g * 255)}, ${Math.round(color.b * 255)})`;
        colorDiv.onclick = () => selectColor(color, colorDiv);
        picker.appendChild(colorDiv);
      });
    }

    function selectColor(color, element) {
      selectedColor = color;
      document.querySelectorAll('.color-option').forEach(el => el.classList.remove('selected'));
      element.classList.add('selected');
    }

    function createShape(type) {
      const name = document.getElementById('shapeName').value || `${type}-${Date.now()}`;

      parent.postMessage({
        pluginMessage: {
          type: 'create-shape',
          shapeType: type,
          properties: {
            name: name,
            fill: selectedColor,
            text: type === 'text' ? 'Hello World' : undefined,
            fontSize: type === 'text' ? 24 : undefined,
            width: 100,
            height: 100
          }
        }
      }, '*');

      updateStatus(`Created ${type}: ${name}`);
    }

    function getSelection() {
      parent.postMessage({ pluginMessage: { type: 'get-selection' } }, '*');
    }

    function clearSelection() {
      parent.postMessage({
        pluginMessage: {
          type: 'update-selection',
          nodeIds: []
        }
      }, '*');
    }

    function exportSelection(format) {
      parent.postMessage({
        pluginMessage: {
          type: 'export-selection',
          format: format
        }
      }, '*');
    }

    function duplicateSelection() {
      parent.postMessage({
        pluginMessage: {
          type: 'duplicate-selection'
        }
      }, '*');
      updateStatus('Duplicated selection');
    }

    function groupSelection() {
      parent.postMessage({
        pluginMessage: {
          type: 'group-selection'
        }
      }, '*');
      updateStatus('Grouped selection');
    }

    function alignSelection() {
      parent.postMessage({
        pluginMessage: {
          type: 'align-selection'
        }
      }, '*');
      updateStatus('Aligned selection');
    }

    function deleteSelection() {
      parent.postMessage({
        pluginMessage: {
          type: 'delete-selection'
        }
      }, '*');
      updateStatus('Deleted selection');
    }

    function updateStatus(message) {
      const status = document.getElementById('status');
      status.textContent = message;
      setTimeout(() => {
        status.textContent = 'Ready to create shapes!';
      }, 3000);
    }

    // Handle messages from plugin
    window.onmessage = (event) => {
      const msg = event.data.pluginMessage;
      if (!msg) return;

      switch (msg.type) {
        case 'initial-state':
          document.getElementById('userInfo').textContent = `User: ${msg.user} | Selection: ${msg.selectedNodes}`;
          break;

        case 'selection-updated':
          updateSelectionList(msg.selection);
          document.getElementById('userInfo').textContent = `Selection: ${msg.selection.length} nodes`;
          break;

        case 'shape-created':
          updateStatus(`Created: ${msg.shape.name}`);
          getSelection(); // Refresh selection
          break;

        case 'export-complete':
          downloadFile(msg.data, msg.filename);
          updateStatus(`Exported: ${msg.filename}`);
          break;
      }
    };

    function updateSelectionList(selection) {
      const list = document.getElementById('selectionList');
      if (selection.length === 0) {
        list.innerHTML = '<div style="color: #999; text-align: center;">No selection</div>';
        return;
      }

      list.innerHTML = selection.map(node => `
        <div class="selection-item">
          <span>${node.name}</span>
          <span class="node-type">${node.type}</span>
        </div>
      `).join('');
    }

    function downloadFile(data, filename) {
      if (typeof data === 'string') {
        // JSON/text file
        const blob = new Blob([data], { type: 'text/plain' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = filename;
        a.click();
        URL.revokeObjectURL(url);
      } else {
        // Binary data (PNG, SVG)
        const blob = new Blob([new Uint8Array(data)], { type: 'application/octet-stream' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = filename;
        a.click();
        URL.revokeObjectURL(url);
      }
    }
  </script>
</body>
</html>
*/

💻 Менеджер Системы Дизайна typescript

🟡 intermediate ⭐⭐⭐⭐

Полнофункциональный плагин системы дизайна с библиотекой компонентов, управлением стилями и системой токенов

⏱️ 45 min 🏷️ figma, plugin, typescript, design systems
Prerequisites: Advanced TypeScript, Figma API expertise, Design systems knowledge
// Figma Design System Manager Plugin
// Component library, style management, and design token system

interface DesignToken {
  name: string
  category: 'color' | 'typography' | 'spacing' | 'effects' | 'border'
  value: any
  description?: string
}

interface Component {
  id: string
  name: string
  category: string
  description: string
  properties: Record<string, any>
  preview: string
}

interface StyleGuide {
  name: string
  description: string
  tokens: DesignToken[]
  components: Component[]
  lastUpdated: string
}

// Main plugin code
export default function () {
  const designSystem = loadDesignSystem()

  showUI({
    height: 600,
    width: 400
  }, {
    designSystem,
    currentPage: figma.currentPage.name,
    selectedCount: figma.currentPage.selection.length
  })
}

// Message handling
figma.ui.onmessage = async (msg: any) => {
  switch (msg.type) {
    case 'save-design-system':
      await saveDesignSystem(msg.designSystem)
      figma.notify('Design system saved successfully')
      break

    case 'apply-color-token':
      await applyColorToken(msg.tokenName, msg.target)
      break

    case 'apply-typography-token':
      await applyTypographyToken(msg.tokenName, msg.target)
      break

    case 'apply-spacing-token':
      await applySpacingToken(msg.tokenName, msg.target)
      break

    case 'create-component':
      await createDesignComponent(msg.component)
      break

    case 'generate-style-guide':
      await generateStyleGuide(msg.options)
      break

    case 'extract-tokens':
      await extractTokensFromSelection()
      break

    case 'audit-design':
      await auditDesignSystem()
      break

    case 'sync-with-library':
      await syncWithDesignLibrary(msg.libraryId)
      break
  }
}

// Design System Management
function loadDesignSystem(): StyleGuide {
  const stored = figma.root.getPluginData('designSystem')
  if (stored) {
    try {
      return JSON.parse(stored)
    } catch (e) {
      console.error('Error loading design system:', e)
    }
  }

  // Default design system
  return {
    name: 'My Design System',
    description: 'Default design system',
    tokens: getDefaultTokens(),
    components: [],
    lastUpdated: new Date().toISOString()
  }
}

async function saveDesignSystem(designSystem: StyleGuide) {
  designSystem.lastUpdated = new Date().toISOString()
  figma.root.setPluginData('designSystem', JSON.stringify(designSystem))
}

function getDefaultTokens(): DesignToken[] {
  return [
    // Color tokens
    {
      name: 'primary-blue',
      category: 'color',
      value: { r: 0.13, g: 0.59, b: 1.0 },
      description: 'Primary brand color'
    },
    {
      name: 'secondary-gray',
      category: 'color',
      value: { r: 0.5, g: 0.5, b: 0.5 },
      description: 'Secondary text color'
    },
    {
      name: 'success-green',
      category: 'color',
      value: { r: 0.2, g: 0.8, b: 0.4 },
      description: 'Success state color'
    },
    {
      name: 'error-red',
      category: 'color',
      value: { r: 1.0, g: 0.2, b: 0.2 },
      description: 'Error state color'
    },

    // Typography tokens
    {
      name: 'heading-large',
      category: 'typography',
      value: {
        fontFamily: 'Inter',
        fontWeight: 700,
        fontSize: 32,
        lineHeight: 40
      }
    },
    {
      name: 'body-regular',
      category: 'typography',
      value: {
        fontFamily: 'Inter',
        fontWeight: 400,
        fontSize: 14,
        lineHeight: 20
      }
    },

    // Spacing tokens
    {
      name: 'spacing-xs',
      category: 'spacing',
      value: 4,
      description: 'Extra small spacing'
    },
    {
      name: 'spacing-sm',
      category: 'spacing',
      value: 8,
      description: 'Small spacing'
    },
    {
      name: 'spacing-md',
      category: 'spacing',
      value: 16,
      description: 'Medium spacing'
    },
    {
      name: 'spacing-lg',
      category: 'spacing',
      value: 24,
      description: 'Large spacing'
    },

    // Effect tokens
    {
      name: 'shadow-sm',
      category: 'effects',
      value: {
        type: 'DROP_SHADOW',
        color: { r: 0, g: 0, b: 0, a: 0.1 },
        offset: { x: 0, y: 2 },
        radius: 4
      }
    },

    // Border tokens
    {
      name: 'border-radius-sm',
      category: 'border',
      value: 4
    },
    {
      name: 'border-radius-md',
      category: 'border',
      value: 8
    }
  ]
}

// Token Application Functions
async function applyColorToken(tokenName: string, target: string = 'selection') {
  const designSystem = loadDesignSystem()
  const token = designSystem.tokens.find(t => t.name === tokenName && t.category === 'color')

  if (!token) {
    figma.notify('Color token not found')
    return
  }

  const nodes = target === 'selection' ? figma.currentPage.selection : [figma.getNodeById(target)]

  for (const node of nodes) {
    if ('fills' in node) {
      node.fills = [{
        type: 'SOLID',
        color: token.value
      }]

      // Add token name as comment for reference
      if (!node.getPluginData('colorToken')) {
        node.setPluginData('colorToken', tokenName)
      }
    }
  }

  figma.notify(`Applied color token: ${tokenName}`)
}

async function applyTypographyToken(tokenName: string, target: string = 'selection') {
  const designSystem = loadDesignSystem()
  const token = designSystem.tokens.find(t => t.name === tokenName && t.category === 'typography')

  if (!token) {
    figma.notify('Typography token not found')
    return
  }

  const nodes = target === 'selection' ? figma.currentPage.selection : [figma.getNodeById(target)]

  for (const node of nodes) {
    if (node.type === 'TEXT') {
      const textNode = node as TextNode

      // Load font
      await figma.loadFontAsync({
        family: token.value.fontFamily,
        style: getFontStyle(token.value.fontWeight)
      })

      // Apply typography styles
      textNode.fontName = { family: token.value.fontFamily, style: getFontStyle(token.value.fontWeight) }
      textNode.fontSize = token.value.fontSize
      textNode.lineHeight = { unit: 'PIXELS', value: token.value.lineHeight }

      // Store token reference
      textNode.setPluginData('typographyToken', tokenName)
    }
  }

  figma.notify(`Applied typography token: ${tokenName}`)
}

async function applySpacingToken(tokenName: string, target: string = 'selection') {
  const designSystem = loadDesignSystem()
  const token = designSystem.tokens.find(t => t.name === tokenName && t.category === 'spacing')

  if (!token) {
    figma.notify('Spacing token not found')
    return
  }

  const nodes = target === 'selection' ? figma.currentPage.selection : [figma.getNodeById(target)]

  for (const node of nodes) {
    if ('layoutMode' in node && node.layoutMode !== 'NONE') {
      // Auto layout node
      const frameNode = node as FrameNode
      if (frameNode.itemSpacing !== token.value) {
        frameNode.itemSpacing = token.value
      }
    } else {
      // Regular node - apply padding
      if ('paddingTop' in node) {
        const containerNode = node as FrameNode
        containerNode.paddingTop = token.value
        containerNode.paddingBottom = token.value
        containerNode.paddingLeft = token.value
        containerNode.paddingRight = token.value
      }
    }

    node.setPluginData('spacingToken', tokenName)
  }

  figma.notify(`Applied spacing token: ${tokenName}`)
}

function getFontStyle(weight: number): string {
  const styles = {
    100: 'Thin',
    200: 'Extra Light',
    300: 'Light',
    400: 'Regular',
    500: 'Medium',
    600: 'Semi Bold',
    700: 'Bold',
    800: 'Extra Bold',
    900: 'Black'
  }
  return styles[weight] || 'Regular'
}

// Component Creation
async function createDesignComponent(component: Component) {
  // Create component based on type
  switch (component.category) {
    case 'button':
      await createButtonComponent(component)
      break
    case 'input':
      await createInputComponent(component)
      break
    case 'card':
      await createCardComponent(component)
      break
    case 'avatar':
      await createAvatarComponent(component)
      break
    default:
      await createGenericComponent(component)
  }
}

async function createButtonComponent(component: Component) {
  const designSystem = loadDesignSystem()

  // Create button group
  const buttonGroup = figma.createFrame()
  buttonGroup.name = component.name
  buttonGroup.resize(component.properties.width || 120, component.properties.height || 40)

  // Apply button styles
  const primaryToken = designSystem.tokens.find(t => t.name === 'primary-blue')
  if (primaryToken) {
    buttonGroup.fills = [{
      type: 'SOLID',
      color: primaryToken.value
    }]
  }

  const borderRadiusToken = designSystem.tokens.find(t => t.name === 'border-radius-md')
  if (borderRadiusToken) {
    buttonGroup.cornerRadius = borderRadiusToken.value
  }

  // Add text label
  const textNode = figma.createText()
  await figma.loadFontAsync({ family: 'Inter', style: 'Medium' })
  textNode.characters = component.properties.text || 'Button'
  textNode.fontSize = 14
  textNode.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }]

  // Center text in button
  textNode.x = (buttonGroup.width - textNode.width) / 2
  textNode.y = (buttonGroup.height - textNode.height) / 2

  buttonGroup.appendChild(textNode)
  figma.currentPage.appendChild(buttonGroup)

  // Convert to component
  const componentNode = buttonGroup.createComponent()
  figma.currentPage.selection = [componentNode]

  figma.notify(`Created button component: ${component.name}`)
}

async function createInputComponent(component: Component) {
  const designSystem = loadDesignSystem()

  // Create input frame
  const inputFrame = figma.createFrame()
  inputFrame.name = component.name
  inputFrame.resize(component.properties.width || 200, component.properties.height || 40)

  // Apply input styles
  inputFrame.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }]
  inputFrame.strokes = [{
    type: 'SOLID',
    color: { r: 0.8, g: 0.8, b: 0.8 }
  }]

  const borderRadiusToken = designSystem.tokens.find(t => t.name === 'border-radius-sm')
  if (borderRadiusToken) {
    inputFrame.cornerRadius = borderRadiusToken.value
  }

  // Add placeholder text
  const placeholderText = figma.createText()
  await figma.loadFontAsync({ family: 'Inter', style: 'Regular' })
  placeholderText.characters = component.properties.placeholder || 'Enter text...'
  placeholderText.fontSize = 14
  placeholderText.fills = [{ type: 'SOLID', color: { r: 0.6, g: 0.6, b: 0.6 } }]

  placeholderText.x = 12
  placeholderText.y = (inputFrame.height - placeholderText.height) / 2

  inputFrame.appendChild(placeholderText)
  figma.currentPage.appendChild(inputFrame)

  figma.notify(`Created input component: ${component.name}`)
}

async function createCardComponent(component: Component) {
  const designSystem = loadDesignSystem()

  // Create card frame
  const cardFrame = figma.createFrame()
  cardFrame.name = component.name
  cardFrame.resize(component.properties.width || 300, component.properties.height || 200)

  // Apply card styles
  cardFrame.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }]

  const shadowToken = designSystem.tokens.find(t => t.name === 'shadow-sm')
  if (shadowToken) {
    cardFrame.effects = [shadowToken.value]
  }

  const borderRadiusToken = designSystem.tokens.find(t => t.name === 'border-radius-md')
  if (borderRadiusToken) {
    cardFrame.cornerRadius = borderRadiusToken.value
  }

  figma.currentPage.appendChild(cardFrame)

  figma.notify(`Created card component: ${component.name}`)
}

async function createAvatarComponent(component: Component) {
  const avatarFrame = figma.createFrame()
  avatarFrame.name = component.name
  const size = component.properties.size || 40
  avatarFrame.resize(size, size)

  // Make circular
  avatarFrame.cornerRadius = size / 2

  // Random color or specified color
  const color = component.properties.color || { r: 0.7, g: 0.7, b: 0.7 }
  avatarFrame.fills = [{ type: 'SOLID', color }]

  figma.currentPage.appendChild(avatarFrame)

  figma.notify(`Created avatar component: ${component.name}`)
}

async function createGenericComponent(component: Component) {
  const frame = figma.createFrame()
  frame.name = component.name

  if (component.properties.width) frame.resize(component.properties.width, component.properties.height || 100)

  figma.currentPage.appendChild(frame)

  figma.notify(`Created component: ${component.name}`)
}

// Style Guide Generation
async function generateStyleGuide(options: any) {
  const designSystem = loadDesignSystem()

  // Create style guide page
  const styleGuidePage = figma.createPage()
  styleGuidePage.name = `${designSystem.name} Style Guide`

  let currentY = 50

  // Title
  const titleText = figma.createText()
  await figma.loadFontAsync({ family: 'Inter', style: 'Bold' })
  titleText.characters = designSystem.name
  titleText.fontSize = 48
  titleText.x = 50
  titleText.y = currentY
  styleGuidePage.appendChild(titleText)

  currentY += 100

  // Description
  const descText = figma.createText()
  await figma.loadFontAsync({ family: 'Inter', style: 'Regular' })
  descText.characters = designSystem.description
  descText.fontSize = 16
  descText.x = 50
  descText.y = currentY
  styleGuidePage.appendChild(descText)

  currentY += 80

  // Generate sections for each token category
  const categories = ['color', 'typography', 'spacing', 'effects', 'border']

  for (const category of categories) {
    const categoryTokens = designSystem.tokens.filter(t => t.category === category)
    if (categoryTokens.length === 0) continue

    // Section title
    const sectionTitle = figma.createText()
    await figma.loadFontAsync({ family: 'Inter', style: 'Semi Bold' })
    sectionTitle.characters = category.charAt(0).toUpperCase() + category.slice(1) + ' Tokens'
    sectionTitle.fontSize = 24
    sectionTitle.x = 50
    sectionTitle.y = currentY
    styleGuidePage.appendChild(sectionTitle)

    currentY += 50

    // Create token displays
    for (const token of categoryTokens) {
      await createTokenDisplay(token, currentY, styleGuidePage)
      currentY += 60
    }

    currentY += 40
  }

  // Navigate to style guide page
  figma.currentPage = styleGuidePage

  figma.notify('Style guide generated successfully')
}

async function createTokenDisplay(token: DesignToken, y: number, page: PageNode) {
  switch (token.category) {
    case 'color':
      await createColorTokenDisplay(token, y, page)
      break
    case 'typography':
      await createTypographyTokenDisplay(token, y, page)
      break
    case 'spacing':
      await createSpacingTokenDisplay(token, y, page)
      break
    default:
      await createGenericTokenDisplay(token, y, page)
  }
}

async function createColorTokenDisplay(token: DesignToken, y: number, page: PageNode) {
  // Color swatch
  const swatch = figma.createRectangle()
  swatch.resize(60, 60)
  swatch.x = 50
  swatch.y = y
  swatch.fills = [{ type: 'SOLID', color: token.value }]
  page.appendChild(swatch)

  // Token name
  const nameText = figma.createText()
  await figma.loadFontAsync({ family: 'Inter', style: 'Medium' })
  nameText.characters = token.name
  nameText.fontSize = 14
  nameText.x = 130
  nameText.y = y
  page.appendChild(nameText)

  // Token value
  const valueText = figma.createText()
  await figma.loadFontAsync({ family: 'Inter', style: 'Regular' })
  valueText.characters = `RGB(${Math.round(token.value.r * 255)}, ${Math.round(token.value.g * 255)}, ${Math.round(token.value.b * 255)})`
  valueText.fontSize = 12
  valueText.fills = [{ type: 'SOLID', color: { r: 0.6, g: 0.6, b: 0.6 } }]
  valueText.x = 130
  valueText.y = y + 20
  page.appendChild(valueText)

  // Description
  if (token.description) {
    const descText = figma.createText()
    await figma.loadFontAsync({ family: 'Inter', style: 'Regular' })
    descText.characters = token.description
    descText.fontSize = 12
    descText.fills = [{ type: 'SOLID', color: { r: 0.4, g: 0.4, b: 0.4 } }]
    descText.x = 130
    descText.y = y + 40
    page.appendChild(descText)
  }
}

async function createTypographyTokenDisplay(token: DesignToken, y: number, page: PageNode) {
  const sampleText = figma.createText()
  await figma.loadFontAsync({
    family: token.value.fontFamily,
    style: getFontStyle(token.value.fontWeight)
  })
  sampleText.characters = 'The quick brown fox'
  sampleText.fontSize = token.value.fontSize
  sampleText.x = 50
  sampleText.y = y
  page.appendChild(sampleText)

  // Token info
  const infoText = figma.createText()
  await figma.loadFontAsync({ family: 'Inter', style: 'Regular' })
  infoText.characters = `${token.name} - ${token.value.fontFamily} ${token.value.fontWeight}`
  infoText.fontSize = 12
  infoText.fills = [{ type: 'SOLID', color: { r: 0.6, g: 0.6, b: 0.6 } }]
  infoText.x = 50
  infoText.y = y + token.value.fontSize + 10
  page.appendChild(infoText)
}

async function createSpacingTokenDisplay(token: DesignToken, y: number, page: PageNode) {
  const spacingBox = figma.createRectangle()
  spacingBox.resize(token.value, token.value)
  spacingBox.x = 50
  spacingBox.y = y
  spacingBox.fills = [{ type: 'SOLID', color: { r: 0.9, g: 0.9, b: 0.9 } }]
  page.appendChild(spacingBox)

  // Token info
  const infoText = figma.createText()
  await figma.loadFontAsync({ family: 'Inter', style: 'Regular' })
  infoText.characters = `${token.name}: ${token.value}px`
  infoText.fontSize = 14
  infoText.x = token.value + 70
  infoText.y = y + token.value / 2 - 7
  page.appendChild(infoText)
}

async function createGenericTokenDisplay(token: DesignToken, y: number, page: PageNode) {
  const infoText = figma.createText()
  await figma.loadFontAsync({ family: 'Inter', style: 'Medium' })
  infoText.characters = `${token.name}: ${JSON.stringify(token.value)}`
  infoText.fontSize = 14
  infoText.x = 50
  infoText.y = y
  page.appendChild(infoText)
}

// Token Extraction
async function extractTokensFromSelection() {
  if (figma.currentPage.selection.length === 0) {
    figma.notify('Please select objects to extract tokens from')
    return
  }

  const extractedTokens: DesignToken[] = []

  for (const node of figma.currentPage.selection) {
    // Extract color tokens
    if ('fills' in node) {
      for (const fill of node.fills) {
        if (fill.type === 'SOLID') {
          const token: DesignToken = {
            name: `${node.name.toLowerCase().replace(/\s+/g, '-')}-color`,
            category: 'color',
            value: fill.color,
            description: `Extracted from ${node.name}`
          }
          extractedTokens.push(token)
        }
      }
    }

    // Extract typography tokens
    if (node.type === 'TEXT') {
      const textNode = node as TextNode
      const token: DesignToken = {
        name: `${node.name.toLowerCase().replace(/\s+/g, '-')}-typography`,
        category: 'typography',
        value: {
          fontFamily: textNode.fontName.family,
          fontWeight: getFontWeight(textNode.fontName.style),
          fontSize: textNode.fontSize,
          lineHeight: textNode.lineHeight?.value || textNode.fontSize * 1.4
        },
        description: `Extracted from ${node.name}`
      }
      extractedTokens.push(token)
    }

    // Extract spacing tokens
    if ('paddingTop' in node) {
      const frameNode = node as FrameNode
      const token: DesignToken = {
        name: `${node.name.toLowerCase().replace(/\s+/g, '-')}-padding`,
        category: 'spacing',
        value: frameNode.paddingTop,
        description: `Extracted from ${node.name}`
      }
      extractedTokens.push(token)
    }
  }

  // Send extracted tokens to UI
  figma.ui.postMessage({
    type: 'tokens-extracted',
    tokens: extractedTokens
  })

  figma.notify(`Extracted ${extractedTokens.length} tokens`)
}

function getFontWeight(style: string): number {
  const weights: Record<string, number> = {
    'Thin': 100,
    'Extra Light': 200,
    'Light': 300,
    'Regular': 400,
    'Medium': 500,
    'Semi Bold': 600,
    'Bold': 700,
    'Extra Bold': 800,
    'Black': 900
  }
  return weights[style] || 400
}

// Design System Audit
async function auditDesignSystem() {
  const designSystem = loadDesignSystem()
  const issues = []

  // Audit all nodes in current page
  const allNodes = figma.currentPage.findAll(node => true)

  for (const node of allNodes) {
    // Check for inconsistent colors
    if ('fills' in node) {
      for (const fill of node.fills) {
        if (fill.type === 'SOLID') {
          const matchingToken = designSystem.tokens.find(t =>
            t.category === 'color' &&
            JSON.stringify(t.value) === JSON.stringify(fill.color)
          )

          if (!matchingToken) {
            issues.push({
              type: 'inconsistent-color',
              node: node.name,
              color: fill.color
            })
          }
        }
      }
    }

    // Check for inconsistent typography
    if (node.type === 'TEXT') {
      const textNode = node as TextNode
      const matchingToken = designSystem.tokens.find(t =>
        t.category === 'typography' &&
        t.value.fontFamily === textNode.fontName.family &&
        t.value.fontSize === textNode.fontSize
      )

      if (!matchingToken) {
        issues.push({
          type: 'inconsistent-typography',
          node: node.name,
          fontFamily: textNode.fontName.family,
          fontSize: textNode.fontSize
        })
      }
    }
  }

  // Send audit results to UI
  figma.ui.postMessage({
    type: 'audit-complete',
    issues
  })

  figma.notify(`Audit complete. Found ${issues.length} issues`)
}

// Library Sync (placeholder)
async function syncWithDesignLibrary(libraryId: string) {
  figma.notify('Library sync functionality would be implemented here')
  // This would integrate with Figma's team libraries API
}

💻 Инструменты Автоматизации Дизайна typescript

🔴 complex ⭐⭐⭐⭐⭐

Продвинутые инструменты автоматизации для пакетной обработки, очистки дизайна и оптимизации рабочих процессов

⏱️ 50 min 🏷️ figma, plugin, typescript, automation, batch
Prerequisites: Expert TypeScript, Figma API mastery, Automation concepts
// Figma Design Automation Tools
// Batch processing, design cleanup, and workflow optimization

interface AutomationRule {
  name: string
  description: string
  category: 'cleanup' | 'organization' | 'naming' | 'accessibility' | 'performance'
  enabled: boolean
  action: Function
}

interface BatchOperation {
  id: string
  name: string
  description: string
  operation: Function
  progress: number
  status: 'pending' | 'running' | 'completed' | 'error'
}

// Main plugin code
export default function () {
  showUI({
    height: 700,
    width: 450
  }, {
    rules: getAutomationRules(),
    operations: getBatchOperations(),
    pageStats: getPageStatistics()
  })
}

// Message handling
figma.ui.onmessage = async (msg: any) => {
  switch (msg.type) {
    case 'run-automation':
      await runAutomationRules(msg.rules, msg.scope)
      break

    case 'batch-operation':
      await executeBatchOperation(msg.operationId, msg.options)
      break

    case 'generate-report':
      await generateDesignReport(msg.options)
      break

    case 'optimize-performance':
      await optimizePerformance()
      break

    case 'check-accessibility':
      await checkAccessibility()
      break

    case 'export-assets':
      await exportAssets(msg.options)
      break

    case 'rename-nodes':
      await batchRenameNodes(msg.pattern, msg.replacement)
      break
  }
}

// Automation Rules
function getAutomationRules(): AutomationRule[] {
  return [
    {
      name: 'Remove Unused Layers',
      description: 'Remove hidden and empty layers to reduce file size',
      category: 'cleanup',
      enabled: true,
      action: removeUnusedLayers
    },
    {
      name: 'Standardize Naming',
      description: 'Rename layers to follow consistent naming conventions',
      category: 'naming',
      enabled: true,
      action: standardizeNaming
    },
    {
      name: 'Group Similar Elements',
      description: 'Group related elements together for better organization',
      category: 'organization',
      enabled: false,
      action: groupSimilarElements
    },
    {
      name: 'Check Color Contrast',
      description: 'Verify text elements meet accessibility contrast ratios',
      category: 'accessibility',
      enabled: true,
      action: checkColorContrast
    },
    {
      name: 'Optimize Images',
      description: 'Compress and optimize images for better performance',
      category: 'performance',
      enabled: false,
      action: optimizeImages
    },
    {
      name: 'Remove Duplicate Styles',
      description: 'Find and remove duplicate text and color styles',
      category: 'cleanup',
      enabled: true,
      action: removeDuplicateStyles
    }
  ]
}

// Cleanup Functions
async function removeUnusedLayers(scope: 'page' | 'selection' = 'page') {
  const nodes = scope === 'page' ? figma.currentPage.findAll(n => true) : figma.currentPage.selection
  const removedCount = 0

  for (const node of nodes) {
    let shouldRemove = false

    // Check if node is hidden and has no children
    if (!node.visible && node.type !== 'PAGE') {
      shouldRemove = true
    }

    // Check if frame is empty
    if (node.type === 'FRAME' && node.children.length === 0) {
      shouldRemove = true
    }

    // Check if layer is completely outside canvas bounds
    if (node.x > figma.viewport.bounds.width + 1000 ||
        node.y > figma.viewport.bounds.height + 1000 ||
        node.x + node.width < -1000 ||
        node.y + node.height < -1000) {
      shouldRemove = true
    }

    if (shouldRemove) {
      node.remove()
      removedCount++
    }
  }

  figma.notify(`Removed ${removedCount} unused layers`)
  return removedCount
}

async function standardizeNaming(scope: 'page' | 'selection' = 'page') {
  const nodes = scope === 'page' ? figma.currentPage.findAll(n => true) : figma.currentPage.selection
  let renamedCount = 0

  for (const node of nodes) {
    if (node.type === 'PAGE') continue

    const originalName = node.name
    let newName = originalName

    // Standardize naming patterns
    newName = newName.replace(/\s+/g, '-') // Replace spaces with hyphens
    newName = newName.replace(/[^a-zA-Z0-9-_]/g, '') // Remove special characters
    newName = newName.toLowerCase() // Convert to lowercase

    // Add prefixes based on type
    if (!newName.match(/^[a-z]+-/)) {
      const prefix = getNodeTypePrefix(node.type)
      newName = `${prefix}-${newName}`
    }

    // Remove multiple consecutive hyphens
    newName = newName.replace(/-+/g, '-')

    // Remove trailing hyphens
    newName = newName.replace(/-+$/, '')

    if (newName !== originalName && newName !== '') {
      node.name = newName
      renamedCount++
    }
  }

  figma.notify(`Renamed ${renamedCount} layers`)
  return renamedCount
}

function getNodeTypePrefix(type: NodeType): string {
  const prefixes = {
    'FRAME': 'frame',
    'GROUP': 'group',
    'COMPONENT': 'component',
    'INSTANCE': 'instance',
    'TEXT': 'text',
    'RECTANGLE': 'rect',
    'ELLIPSE': 'ellipse',
    'POLYGON': 'polygon',
    'STAR': 'star',
    'VECTOR': 'vector',
    'LINE': 'line'
  }
  return prefixes[type] || 'layer'
}

async function groupSimilarElements(scope: 'page' | 'selection' = 'page') {
  const nodes = scope === 'page' ? figma.currentPage.findAll(n => true) : figma.currentPage.selection
  const groups = new Map<string, SceneNode[]>()

  // Group elements by type and similar properties
  for (const node of nodes) {
    if (node.type === 'PAGE') continue

    let groupKey = node.type

    // Additional grouping criteria
    if (node.type === 'TEXT') {
      const textNode = node as TextNode
      groupKey += `-${textNode.fontSize}-${textNode.fontName.family}`
    }

    if ('fills' in node && node.fills.length > 0) {
      const fill = node.fills[0]
      if (fill.type === 'SOLID') {
        groupKey += `-${Math.round(fill.color.r * 255)}-${Math.round(fill.color.g * 255)}-${Math.round(fill.color.b * 255)}`
      }
    }

    if (!groups.has(groupKey)) {
      groups.set(groupKey, [])
    }
    groups.get(groupKey)!.push(node)
  }

  // Create groups for similar elements
  let groupCount = 0
  for (const [key, elements] of groups) {
    if (elements.length > 1) {
      const groupFrame = figma.createFrame()
      groupFrame.name = `group-${key.toLowerCase().replace(/[^a-zA-Z0-9]/g, '-')}`

      // Calculate group bounds
      let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity

      for (const element of elements) {
        minX = Math.min(minX, element.x)
        minY = Math.min(minY, element.y)
        maxX = Math.max(maxX, element.x + element.width)
        maxY = Math.max(maxY, element.y + element.height)
      }

      // Position and size the group
      groupFrame.x = minX - 20
      groupFrame.y = minY - 20
      groupFrame.resize(maxX - minX + 40, maxY - minY + 40)

      // Add elements to group
      for (const element of elements) {
        groupFrame.appendChild(element)
      }

      figma.currentPage.appendChild(groupFrame)
      groupCount++
    }
  }

  figma.notify(`Created ${groupCount} groups`)
  return groupCount
}

// Accessibility Functions
async function checkColorContrast(scope: 'page' | 'selection' = 'page') {
  const nodes = scope === 'page' ? figma.currentPage.findAll(n => n.type === 'TEXT') :
                figma.currentPage.selection.filter(n => n.type === 'TEXT')

  const issues = []

  for (const node of nodes) {
    const textNode = node as TextNode
    const backgroundColor = getBackgroundColor(textNode)

    if (backgroundColor) {
      const contrastRatio = calculateContrastRatio(
        textNode.fills[0]?.color || { r: 0, g: 0, b: 0 },
        backgroundColor
      )

      const isLargeText = textNode.fontSize >= 18
      const minimumRatio = isLargeText ? 3.0 : 4.5

      if (contrastRatio < minimumRatio) {
        issues.push({
          node: textNode.name,
          contrast: contrastRatio,
          minimum: minimumRatio,
          fontSize: textNode.fontSize
        })
      }
    }
  }

  figma.ui.postMessage({
    type: 'accessibility-report',
    issues
  })

  figma.notify(`Found ${issues.length} contrast issues`)
  return issues
}

function getBackgroundColor(textNode: TextNode): Color | null {
  let current: SceneNode | null = textNode

  while (current) {
    if ('fills' in current && current.fills.length > 0) {
      const fill = current.fills[0]
      if (fill.type === 'SOLID') {
        return fill.color
      }
    }
    current = current.parent
  }

  return null
}

function calculateContrastRatio(color1: Color, color2: Color): number {
  const luminance1 = calculateLuminance(color1)
  const luminance2 = calculateLuminance(color2)

  const lighter = Math.max(luminance1, luminance2)
  const darker = Math.min(luminance1, luminance2)

  return (lighter + 0.05) / (darker + 0.05)
}

function calculateLuminance(color: Color): number {
  const rsRGB = color.r
  const gsRGB = color.g
  const bsRGB = color.b

  const r = rsRGB <= 0.03928 ? rsRGB / 12.92 : Math.pow((rsRGB + 0.055) / 1.055, 2.4)
  const g = gsRGB <= 0.03928 ? gsRGB / 12.92 : Math.pow((gsRGB + 0.055) / 1.055, 2.4)
  const b = bsRGB <= 0.03928 ? bsRGB / 12.92 : Math.pow((bsRGB + 0.055) / 1.055, 2.4)

  return 0.2126 * r + 0.7152 * g + 0.0722 * b
}

// Performance Functions
async function optimizeImages() {
  const imageNodes = figma.currentPage.findAll(n =>
    ('fills' in n) && n.fills.some(fill => fill.type === 'IMAGE')
  )

  let optimizedCount = 0

  for (const node of imageNodes) {
    if ('fills' in node) {
      const newFills = [...node.fills]

      for (let i = 0; i < newFills.length; i++) {
        if (newFills[i].type === 'IMAGE') {
          // In a real implementation, you would compress the image here
          // For now, we'll just mark it as optimized
          optimizedCount++
        }
      }

      node.fills = newFills
    }
  }

  figma.notify(`Optimized ${optimizedCount} images`)
  return optimizedCount
}

async function removeDuplicateStyles() {
  const allTextNodes = figma.currentPage.findAll(n => n.type === 'TEXT') as TextNode[]
  const styles = new Map<string, TextNode[]>()

  // Group similar text styles
  for (const node of allTextNodes) {
    const styleKey = getTextStyleKey(node)

    if (!styles.has(styleKey)) {
      styles.set(styleKey, [])
    }
    styles.get(styleKey)!.push(node)
  }

  let duplicateCount = 0

  for (const [styleKey, nodes] of styles) {
    if (nodes.length > 1) {
      // Create a shared style for duplicates
      const firstNode = nodes[0]

      // Check if style already exists
      const existingStyle = figma.getLocalTextStyles().find(style =>
        style.fontName.family === firstNode.fontName.family &&
        style.fontName.style === firstNode.fontName.style &&
        style.fontSize === firstNode.fontSize
      )

      if (!existingStyle) {
        const newStyle = figma.createTextStyle()
        newStyle.name = `Style-${styleKey}`
        newStyle.fontName = firstNode.fontName
        newStyle.fontSize = firstNode.fontSize
        newStyle.letterSpacing = firstNode.letterSpacing
        newStyle.lineHeight = firstNode.lineHeight
        newStyle.paragraphIndent = firstNode.paragraphIndent
        newStyle.paragraphSpacing = firstNode.paragraphSpacing
        newStyle.textCase = firstNode.textCase
        newStyle.textDecoration = firstNode.textDecoration

        // Apply style to all nodes with this style
        for (const node of nodes) {
          node.textStyleId = newStyle.id
        }

        duplicateCount += nodes.length - 1
      }
    }
  }

  figma.notify(`Consolidated ${duplicateCount} duplicate styles`)
  return duplicateCount
}

function getTextStyleKey(node: TextNode): string {
  return `${node.fontName.family}-${node.fontName.style}-${node.fontSize}-${node.lineHeight?.value || 'auto'}`
}

// Batch Operations
function getBatchOperations(): BatchOperation[] {
  return [
    {
      id: 'export-components',
      name: 'Export All Components',
      description: 'Export all components as SVG files',
      operation: exportAllComponents,
      progress: 0,
      status: 'pending'
    },
    {
      id: 'resize-artboards',
      name: 'Resize All Artboards',
      description: 'Resize all artboards to specified dimensions',
      operation: resizeAllArtboards,
      progress: 0,
      status: 'pending'
    },
    {
      id: 'generate-thumbnails',
      name: 'Generate Thumbnails',
      description: 'Create thumbnail previews for all frames',
      operation: generateThumbnails,
      progress: 0,
      status: 'pending'
    }
  ]
}

async function executeBatchOperation(operationId: string, options: any) {
  const operations = getBatchOperations()
  const operation = operations.find(op => op.id === operationId)

  if (!operation) {
    figma.notify('Operation not found')
    return
  }

  operation.status = 'running'
  figma.ui.postMessage({
    type: 'operation-status',
    operationId,
    status: 'running'
  })

  try {
    await operation.operation(options)

    operation.status = 'completed'
    operation.progress = 100

    figma.ui.postMessage({
      type: 'operation-status',
      operationId,
      status: 'completed',
      progress: 100
    })

    figma.notify(`Completed: ${operation.name}`)
  } catch (error) {
    operation.status = 'error'

    figma.ui.postMessage({
      type: 'operation-status',
      operationId,
      status: 'error',
      error: error.message
    })

    figma.notify(`Error in ${operation.name}: ${error.message}`)
  }
}

// Batch Operation Implementations
async function exportAllComponents(options: any) {
  const components = figma.currentPage.findAll(n => n.type === 'COMPONENT')

  for (let i = 0; i < components.length; i++) {
    const component = components[i]

    if ('exportAsync' in component) {
      const bytes = await component.exportAsync({
        format: 'SVG',
        contentsOnly: true
      })

      figma.ui.postMessage({
        type: 'export-progress',
        operationId: 'export-components',
        progress: (i + 1) / components.length * 100,
        file: {
          name: `${component.name}.svg`,
          data: Array.from(bytes)
        }
      })
    }
  }
}

async function resizeAllArtboards(options: { width: number; height: number }) {
  const frames = figma.currentPage.findAll(n => n.type === 'FRAME')

  for (let i = 0; i < frames.length; i++) {
    const frame = frames[i] as FrameNode
    frame.resize(options.width, options.height)

    figma.ui.postMessage({
      type: 'export-progress',
      operationId: 'resize-artboards',
      progress: (i + 1) / frames.length * 100
    })
  }
}

async function generateThumbnails(options: any) {
  const frames = figma.currentPage.findAll(n => n.type === 'FRAME')

  for (let i = 0; i < frames.length; i++) {
    const frame = frames[i]

    if ('exportAsync' in frame) {
      const bytes = await frame.exportAsync({
        format: 'PNG',
        constraint: { type: 'SCALE', value: 0.2 }
      })

      figma.ui.postMessage({
        type: 'export-progress',
        operationId: 'generate-thumbnails',
        progress: (i + 1) / frames.length * 100,
        file: {
          name: `${frame.name}-thumb.png`,
          data: Array.from(bytes)
        }
      })
    }
  }
}

// Reporting Functions
async function generateDesignReport(options: any) {
  const report = {
    page: {
      name: figma.currentPage.name,
      nodeCount: figma.currentPage.findAll(n => true).length,
      componentCount: figma.currentPage.findAll(n => n.type === 'COMPONENT').length,
      frameCount: figma.currentPage.findAll(n => n.type === 'FRAME').length
    },
    styles: {
      textStyles: figma.getLocalTextStyles().length,
      colorStyles: figma.getLocalPaintStyles().length,
      effectStyles: figma.getLocalEffectStyles().length
    },
    issues: []
  }

  // Analyze potential issues
  report.issues = await analyzeDesignIssues()

  figma.ui.postMessage({
    type: 'report-generated',
    report
  })
}

async function analyzeDesignIssues(): Promise<any[]> {
  const issues = []
  const allNodes = figma.currentPage.findAll(n => true)

  // Check for very large images
  for (const node of allNodes) {
    if ('fills' in node) {
      for (const fill of node.fills) {
        if (fill.type === 'IMAGE' && fill.imageHash) {
          // Check image size (simplified)
          if (node.width * node.height > 4000000) { // > 4MP
            issues.push({
              type: 'large-image',
              node: node.name,
              size: node.width * node.height,
              recommendation: 'Consider compressing this image'
            })
          }
        }
      }
    }
  }

  // Check for deep nesting
  const maxDepth = findMaxDepth(figma.currentPage)
  if (maxDepth > 10) {
    issues.push({
      type: 'deep-nesting',
      depth: maxDepth,
      recommendation: 'Consider flattening the structure to improve performance'
    })
  }

  // Check for very small text
  const smallTexts = figma.currentPage.findAll(n =>
    n.type === 'TEXT' && (n as TextNode).fontSize < 8
  )
  if (smallTexts.length > 0) {
    issues.push({
      type: 'small-text',
      count: smallTexts.length,
      recommendation: 'Very small text may be hard to read'
    })
  }

  return issues
}

function findMaxDepth(node: SceneNode, currentDepth = 0): number {
  if ('children' in node) {
    let maxChildDepth = currentDepth
    for (const child of node.children) {
      maxChildDepth = Math.max(maxChildDepth, findMaxDepth(child, currentDepth + 1))
    }
    return maxChildDepth
  }
  return currentDepth
}

// Utility Functions
function getPageStatistics() {
  return {
    nodeCount: figma.currentPage.findAll(n => true).length,
    componentCount: figma.currentPage.findAll(n => n.type === 'COMPONENT').length,
    textCount: figma.currentPage.findAll(n => n.type === 'TEXT').length,
    frameCount: figma.currentPage.findAll(n => n.type === 'FRAME').length,
    imageCount: figma.currentPage.findAll(n =>
      ('fills' in n) && n.fills.some(f => f.type === 'IMAGE')
    ).length
  }
}

// Additional utility functions
async function batchRenameNodes(pattern: string, replacement: string) {
  const nodes = figma.currentPage.selection.length > 0 ?
    figma.currentPage.selection : figma.currentPage.findAll(n => true)

  let renamedCount = 0

  for (const node of nodes) {
    if (node.type === 'PAGE') continue

    const oldName = node.name
    const newName = oldName.replace(new RegExp(pattern, 'g'), replacement)

    if (newName !== oldName) {
      node.name = newName
      renamedCount++
    }
  }

  figma.notify(`Renamed ${renamedCount} nodes`)
}

async function exportAssets(options: any) {
  const selection = figma.currentPage.selection.length > 0 ?
    figma.currentPage.selection : figma.currentPage.findAll(n => true)

  for (const node of selection) {
    if ('exportAsync' in node) {
      const bytes = await node.exportAsync(options.exportSettings)

      figma.ui.postMessage({
        type: 'asset-exported',
        asset: {
          name: node.name,
          data: Array.from(bytes)
        }
      })
    }
  }
}