Figma Plugins Samples

Figma plugin development examples with UI creation, design automation, and workflow enhancement tools

Key Facts

Category
Design Tools
Items
3
Format Families
sample

Sample Overview

Figma plugin development examples with UI creation, design automation, and workflow enhancement tools This sample set belongs to Design Tools and can be used to test related workflows inside Elysia Tools.

💻 Figma Hello World Plugin typescript

🟢 simple ⭐⭐

Basic Figma plugin setup with UI creation, API interaction, and message passing fundamentals

⏱️ 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'

function runHelloWorldPlugin() {
  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>
*/

💻 Design System Manager typescript

🟡 intermediate ⭐⭐⭐⭐

Comprehensive design system plugin with component library, style management, and token system

⏱️ 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
function runDesignSystemPlugin() {
  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
}

💻 Design Automation Tools typescript

🔴 complex ⭐⭐⭐⭐⭐

Advanced automation tools for batch processing, design cleanup, and workflow optimization

⏱️ 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
function runDesignAutomationPlugin() {
  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)
        }
      })
    }
  }
}