#
tokens: 48387/50000 11/73 files (page 2/6)
lines: off (toggle) GitHub
raw markdown copy
This is page 2 of 6. Use http://codebase.md/1yhy/figma-context-mcp?page={x} to view the full context.

# Directory Structure

```
├── .editorconfig
├── .env.example
├── .github
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.yml
│   │   └── feature_request.yml
│   ├── PULL_REQUEST_TEMPLATE.md
│   └── workflows
│       └── ci.yml
├── .gitignore
├── .husky
│   ├── commit-msg
│   └── pre-commit
├── .lintstagedrc.json
├── .nvmrc
├── .prettierrc
├── CHANGELOG.md
├── commitlint.config.js
├── CONTRIBUTING.md
├── Dockerfile
├── docs
│   ├── en
│   │   ├── absolute-to-relative-research.md
│   │   ├── architecture.md
│   │   ├── cache-architecture.md
│   │   ├── grid-layout-research.md
│   │   ├── icon-detection.md
│   │   ├── layout-detection-research.md
│   │   └── layout-detection.md
│   └── zh-CN
│       ├── absolute-to-relative-research.md
│       ├── architecture.md
│       ├── cache-architecture.md
│       ├── grid-layout-research.md
│       ├── icon-detection.md
│       ├── layout-detection-research.md
│       ├── layout-detection.md
│       └── TODO-feature-enhancements.md
├── eslint.config.js
├── LICENSE
├── package.json
├── pnpm-lock.yaml
├── README.md
├── README.zh-CN.md
├── scripts
│   ├── fetch-test-data.ts
│   └── optimize-figma-json.ts
├── smithery.yaml
├── src
│   ├── algorithms
│   │   ├── icon
│   │   │   ├── detector.ts
│   │   │   └── index.ts
│   │   └── layout
│   │       ├── detector.ts
│   │       ├── index.ts
│   │       ├── optimizer.ts
│   │       └── spatial.ts
│   ├── config.ts
│   ├── core
│   │   ├── effects.ts
│   │   ├── layout.ts
│   │   ├── parser.ts
│   │   └── style.ts
│   ├── index.ts
│   ├── prompts
│   │   ├── design-to-code.ts
│   │   └── index.ts
│   ├── resources
│   │   ├── figma-resources.ts
│   │   └── index.ts
│   ├── server.ts
│   ├── services
│   │   ├── cache
│   │   │   ├── cache-manager.ts
│   │   │   ├── disk-cache.ts
│   │   │   ├── index.ts
│   │   │   ├── lru-cache.ts
│   │   │   └── types.ts
│   │   ├── cache.ts
│   │   ├── figma.ts
│   │   └── simplify-node-response.ts
│   ├── types
│   │   ├── figma.ts
│   │   ├── index.ts
│   │   └── simplified.ts
│   └── utils
│       ├── color.ts
│       ├── css.ts
│       ├── file.ts
│       └── validation.ts
├── tests
│   ├── fixtures
│   │   ├── expected
│   │   │   ├── node-240-32163-optimized.json
│   │   │   ├── node-402-34955-optimized.json
│   │   │   └── real-node-data-optimized.json
│   │   └── figma-data
│   │       ├── node-240-32163.json
│   │       ├── node-402-34955.json
│   │       └── real-node-data.json
│   ├── integration
│   │   ├── __snapshots__
│   │   │   ├── layout-optimization.test.ts.snap
│   │   │   └── output-quality.test.ts.snap
│   │   ├── layout-optimization.test.ts
│   │   ├── output-quality.test.ts
│   │   └── parser.test.ts
│   ├── unit
│   │   ├── algorithms
│   │   │   ├── icon-optimization.test.ts
│   │   │   ├── icon.test.ts
│   │   │   └── layout.test.ts
│   │   ├── resources
│   │   │   └── figma-resources.test.ts
│   │   └── services
│   │       └── cache.test.ts
│   └── utils
│       ├── preview-generator.ts
│       ├── preview.ts
│       ├── run-simplification.ts
│       └── viewer.html
├── tsconfig.json
├── tsup.config.ts
└── vitest.config.ts
```

# Files

--------------------------------------------------------------------------------
/src/algorithms/layout/spatial.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Spatial Projection Analyzer
 *
 * Analyzes spatial relationships between nodes using 2D projection techniques.
 * Used to detect layout patterns, grouping, and containment relationships.
 *
 * @module algorithms/layout/spatial
 */

import type { SimplifiedNode } from "~/types/index.js";

// ==================== Type Definitions ====================

/**
 * Rectangle representing a node's bounding box
 */
export interface Rect {
  left: number;
  top: number;
  width: number;
  height: number;
}

/**
 * Spatial relationship between two nodes
 */
export enum NodeRelationship {
  /** One node completely contains another */
  CONTAINS = "contains",
  /** Two nodes partially overlap */
  INTERSECTS = "intersects",
  /** Two nodes do not overlap */
  SEPARATE = "separate",
}

/**
 * Projection line for spatial division
 */
export interface ProjectionLine {
  /** Position of the line */
  position: number;
  /** Direction: 'horizontal' or 'vertical' */
  direction: "horizontal" | "vertical";
  /** Indices of nodes intersecting this line */
  nodeIndices: number[];
}

// ==================== Helper Functions ====================

/**
 * Safely parse CSS numeric value
 * @param value - CSS value string
 * @param defaultValue - Default value if parsing fails
 */
function safeParseFloat(value: string | undefined | null, defaultValue: number = 0): number {
  if (!value) return defaultValue;
  const parsed = parseFloat(value);
  return isNaN(parsed) ? defaultValue : parsed;
}

/**
 * Get node position from absolute coordinates or CSS styles
 */
function getNodePosition(node: SimplifiedNode): { left: number; top: number } {
  let left = 0;
  let top = 0;

  // Prefer absolute coordinates
  if (typeof node._absoluteX === "number") {
    left = node._absoluteX;
  } else if (node.cssStyles?.left) {
    left = safeParseFloat(node.cssStyles.left as string);
  }

  if (typeof node._absoluteY === "number") {
    top = node._absoluteY;
  } else if (node.cssStyles?.top) {
    top = safeParseFloat(node.cssStyles.top as string);
  }

  return { left, top };
}

// ==================== Rectangle Utilities ====================

/**
 * Utility class for rectangle operations
 */
export class RectUtils {
  /**
   * Create Rect from SimplifiedNode
   */
  static fromNode(node: SimplifiedNode): Rect | null {
    if (!node.cssStyles || !node.cssStyles.width || !node.cssStyles.height) {
      return null;
    }

    const width = safeParseFloat(node.cssStyles.width as string, 0);
    const height = safeParseFloat(node.cssStyles.height as string, 0);

    // Invalid rectangle if dimensions are zero
    if (width <= 0 || height <= 0) {
      return null;
    }

    const { left, top } = getNodePosition(node);
    return { left, top, width, height };
  }

  /**
   * Check if rectangle A contains rectangle B
   */
  static contains(a: Rect, b: Rect): boolean {
    return (
      a.left <= b.left &&
      a.top <= b.top &&
      a.left + a.width >= b.left + b.width &&
      a.top + a.height >= b.top + b.height
    );
  }

  /**
   * Check if two rectangles intersect
   */
  static intersects(a: Rect, b: Rect): boolean {
    return !(
      a.left + a.width <= b.left ||
      b.left + b.width <= a.left ||
      a.top + a.height <= b.top ||
      b.top + b.height <= a.top
    );
  }

  /**
   * Calculate intersection of two rectangles
   */
  static intersection(a: Rect, b: Rect): Rect | null {
    if (!RectUtils.intersects(a, b)) {
      return null;
    }

    const left = Math.max(a.left, b.left);
    const top = Math.max(a.top, b.top);
    const right = Math.min(a.left + a.width, b.left + b.width);
    const bottom = Math.min(a.top + a.height, b.top + b.height);

    return {
      left,
      top,
      width: right - left,
      height: bottom - top,
    };
  }

  /**
   * Analyze spatial relationship between two rectangles
   */
  static analyzeRelationship(a: Rect, b: Rect): NodeRelationship {
    if (RectUtils.contains(a, b)) {
      return NodeRelationship.CONTAINS;
    } else if (RectUtils.contains(b, a)) {
      return NodeRelationship.CONTAINS;
    } else if (RectUtils.intersects(a, b)) {
      return NodeRelationship.INTERSECTS;
    } else {
      return NodeRelationship.SEPARATE;
    }
  }
}

// ==================== Spatial Projection Analyzer ====================

/**
 * 2D Spatial Projection Analyzer
 *
 * Uses projection lines to analyze spatial relationships
 * and group nodes by rows and columns.
 */
export class SpatialProjectionAnalyzer {
  /**
   * Convert SimplifiedNode array to Rect array
   */
  static nodesToRects(nodes: SimplifiedNode[]): Rect[] {
    return nodes
      .map((node) => RectUtils.fromNode(node))
      .filter((rect): rect is Rect => rect !== null);
  }

  /**
   * Generate horizontal projection lines (for row detection)
   * @param rects - Rectangle array
   * @param tolerance - Coordinate tolerance in pixels
   */
  static generateHorizontalProjectionLines(rects: Rect[], tolerance: number = 1): ProjectionLine[] {
    if (rects.length === 0) return [];

    // Collect all top and bottom Y coordinates
    const yCoordinates: number[] = [];
    rects.forEach((rect) => {
      yCoordinates.push(rect.top);
      yCoordinates.push(rect.top + rect.height);
    });

    // Sort and filter nearby coordinates
    yCoordinates.sort((a, b) => a - b);
    const uniqueYCoordinates: number[] = [];

    for (let i = 0; i < yCoordinates.length; i++) {
      if (i === 0 || Math.abs(yCoordinates[i] - yCoordinates[i - 1]) > tolerance) {
        uniqueYCoordinates.push(yCoordinates[i]);
      }
    }

    // Create projection line for each Y coordinate
    return uniqueYCoordinates.map((y) => {
      const line: ProjectionLine = {
        position: y,
        direction: "horizontal",
        nodeIndices: [],
      };

      // Find all nodes intersecting this line
      for (let i = 0; i < rects.length; i++) {
        const rect = rects[i];
        if (y >= rect.top && y <= rect.top + rect.height) {
          line.nodeIndices.push(i);
        }
      }

      return line;
    });
  }

  /**
   * Generate vertical projection lines (for column detection)
   * @param rects - Rectangle array
   * @param tolerance - Coordinate tolerance in pixels
   */
  static generateVerticalProjectionLines(rects: Rect[], tolerance: number = 1): ProjectionLine[] {
    if (rects.length === 0) return [];

    // Collect all left and right X coordinates
    const xCoordinates: number[] = [];
    rects.forEach((rect) => {
      xCoordinates.push(rect.left);
      xCoordinates.push(rect.left + rect.width);
    });

    // Sort and filter nearby coordinates
    xCoordinates.sort((a, b) => a - b);
    const uniqueXCoordinates: number[] = [];

    for (let i = 0; i < xCoordinates.length; i++) {
      if (i === 0 || Math.abs(xCoordinates[i] - xCoordinates[i - 1]) > tolerance) {
        uniqueXCoordinates.push(xCoordinates[i]);
      }
    }

    // Create projection line for each X coordinate
    return uniqueXCoordinates.map((x) => {
      const line: ProjectionLine = {
        position: x,
        direction: "vertical",
        nodeIndices: [],
      };

      // Find all nodes intersecting this line
      for (let i = 0; i < rects.length; i++) {
        const rect = rects[i];
        if (x >= rect.left && x <= rect.left + rect.width) {
          line.nodeIndices.push(i);
        }
      }

      return line;
    });
  }

  /**
   * Group nodes by rows
   * @param nodes - Node array
   * @param tolerance - Tolerance in pixels
   */
  static groupNodesByRows(nodes: SimplifiedNode[], tolerance: number = 1): SimplifiedNode[][] {
    const rects = this.nodesToRects(nodes);
    if (rects.length === 0) return [nodes];

    const projectionLines = this.generateHorizontalProjectionLines(rects, tolerance);
    const rows: SimplifiedNode[][] = [];

    for (let i = 0; i < projectionLines.length - 1; i++) {
      const currentLine = projectionLines[i];
      const nextLine = projectionLines[i + 1];

      const nodesBetweenLines = new Set<number>();

      // Find nodes completely between these two lines
      for (let j = 0; j < rects.length; j++) {
        const rect = rects[j];
        if (rect.top >= currentLine.position && rect.top + rect.height <= nextLine.position) {
          nodesBetweenLines.add(j);
        }
      }

      if (nodesBetweenLines.size > 0) {
        // Sort nodes left to right
        const rowNodes = Array.from(nodesBetweenLines)
          .map((index) => nodes[index])
          .sort((a, b) => {
            const { left: aLeft } = getNodePosition(a);
            const { left: bLeft } = getNodePosition(b);
            return aLeft - bLeft;
          });

        rows.push(rowNodes);
      }
    }

    // If no rows found, treat all nodes as one row
    if (rows.length === 0) {
      rows.push([...nodes]);
    }

    return rows;
  }

  /**
   * Group row nodes by columns
   * @param rowNodes - Nodes in a row
   * @param tolerance - Tolerance in pixels
   */
  static groupRowNodesByColumns(
    rowNodes: SimplifiedNode[],
    tolerance: number = 1,
  ): SimplifiedNode[][] {
    const rects = this.nodesToRects(rowNodes);
    if (rects.length === 0) return [rowNodes];

    const projectionLines = this.generateVerticalProjectionLines(rects, tolerance);
    const columns: SimplifiedNode[][] = [];

    for (let i = 0; i < projectionLines.length - 1; i++) {
      const currentLine = projectionLines[i];
      const nextLine = projectionLines[i + 1];

      const nodesBetweenLines = new Set<number>();

      // Find nodes completely between these two lines
      for (let j = 0; j < rects.length; j++) {
        const rect = rects[j];
        if (rect.left >= currentLine.position && rect.left + rect.width <= nextLine.position) {
          nodesBetweenLines.add(j);
        }
      }

      if (nodesBetweenLines.size > 0) {
        const colNodes = Array.from(nodesBetweenLines).map((index) => rowNodes[index]);
        columns.push(colNodes);
      }
    }

    // If no columns found, treat all nodes as one column
    if (columns.length === 0) {
      columns.push([...rowNodes]);
    }

    return columns;
  }

  /**
   * Process node spatial relationships and build containment hierarchy
   * @param nodes - Node array
   */
  static processNodeRelationships(nodes: SimplifiedNode[]): SimplifiedNode[] {
    if (nodes.length <= 1) return [...nodes];

    const rects = this.nodesToRects(nodes);
    if (rects.length !== nodes.length) {
      return nodes; // Cannot process all nodes, return as-is
    }

    // Find all containment relationships
    const containsRelations: [number, number][] = [];
    for (let i = 0; i < rects.length; i++) {
      for (let j = 0; j < rects.length; j++) {
        if (i !== j && RectUtils.contains(rects[i], rects[j])) {
          containsRelations.push([i, j]); // Node i contains node j
        }
      }
    }

    // Build containment graph
    const childrenMap = new Map<number, Set<number>>();
    const parentMap = new Map<number, number | null>();

    // Initialize all nodes without parents
    for (let i = 0; i < nodes.length; i++) {
      parentMap.set(i, null);
      childrenMap.set(i, new Set<number>());
    }

    // Process containment relationships
    for (const [parent, child] of containsRelations) {
      childrenMap.get(parent)?.add(child);
      parentMap.set(child, parent);
    }

    // Fix multi-level containment - ensure each node has only its direct parent
    for (const [child, parent] of parentMap.entries()) {
      if (parent === null) continue;

      let currentParent = parent;
      let grandParent = parentMap.get(currentParent);

      while (grandParent !== null && grandParent !== undefined) {
        // If grandparent also directly contains child, remove parent-child direct relation
        if (childrenMap.get(grandParent)?.has(child)) {
          childrenMap.get(currentParent)?.delete(child);
        }

        currentParent = grandParent;
        grandParent = parentMap.get(currentParent);
      }
    }

    // Build new node tree structure
    const rootIndices = Array.from(parentMap.entries())
      .filter(([_, parent]) => parent === null)
      .map(([index]) => index);

    const result: SimplifiedNode[] = [];

    // Recursively build node tree
    const buildNodeTree = (nodeIndex: number): SimplifiedNode => {
      const node = { ...nodes[nodeIndex] };
      const childIndices = Array.from(childrenMap.get(nodeIndex) || []);

      if (childIndices.length > 0) {
        node.children = childIndices.map(buildNodeTree);
      }

      return node;
    };

    // Build from all root nodes
    for (const rootIndex of rootIndices) {
      result.push(buildNodeTree(rootIndex));
    }

    return result;
  }
}

```

--------------------------------------------------------------------------------
/src/services/figma.ts:
--------------------------------------------------------------------------------

```typescript
import fs from "fs";
import path from "path";
import { parseFigmaResponse } from "~/core/parser.js";
import type { SimplifiedDesign } from "~/types/index.js";
import { cacheManager } from "./cache.js";
import type {
  GetImagesResponse,
  GetFileResponse,
  GetFileNodesResponse,
  GetImageFillsResponse,
} from "@figma/rest-api-spec";
import { Logger } from "~/server.js";
import type {
  FigmaError,
  RateLimitInfo,
  FetchImageParams,
  FetchImageFillParams,
} from "~/types/index.js";

// Re-export types for backward compatibility
export type { FigmaError, RateLimitInfo, FetchImageParams, FetchImageFillParams };

// ==================== Internal Types ====================

/**
 * API Response Result (internal use only)
 */
interface ApiResponse<T> {
  data: T;
  rateLimitInfo: RateLimitInfo;
}

// ==================== Utility Functions ====================

/**
 * Validate fileKey format
 */
function validateFileKey(fileKey: string): void {
  if (!fileKey || typeof fileKey !== "string") {
    throw createFigmaError(400, "fileKey is required");
  }
  // Figma fileKey is typically alphanumeric
  if (!/^[a-zA-Z0-9_-]+$/.test(fileKey)) {
    throw createFigmaError(400, `Invalid fileKey format: ${fileKey}`);
  }
}

/**
 * Validate nodeId format
 */
function validateNodeId(nodeId: string): void {
  if (!nodeId || typeof nodeId !== "string") {
    throw createFigmaError(400, "nodeId is required");
  }
  // Figma nodeId format is typically number:number or number-number
  if (!/^[\d:_-]+$/.test(nodeId)) {
    throw createFigmaError(400, `Invalid nodeId format: ${nodeId}`);
  }
}

/**
 * Validate depth parameter
 */
function validateDepth(depth?: number): void {
  if (depth !== undefined) {
    if (typeof depth !== "number" || depth < 1 || depth > 100) {
      throw createFigmaError(400, "depth must be a number between 1 and 100");
    }
  }
}

/**
 * Validate local path security
 */
function validateLocalPath(localPath: string, fileName: string): string {
  const normalizedPath = path.resolve(localPath, fileName);
  const resolvedLocalPath = path.resolve(localPath);

  if (!normalizedPath.startsWith(resolvedLocalPath)) {
    throw createFigmaError(400, "Invalid file path: path traversal detected");
  }

  return normalizedPath;
}

/**
 * Create Figma error
 */
function createFigmaError(
  status: number,
  message: string,
  rateLimitInfo?: RateLimitInfo,
): FigmaError {
  return {
    status,
    err: message,
    rateLimitInfo,
  };
}

/**
 * Extract Rate Limit information from response headers
 */
function extractRateLimitInfo(headers: Headers): RateLimitInfo {
  return {
    remaining: headers.has("x-rate-limit-remaining")
      ? parseInt(headers.get("x-rate-limit-remaining")!, 10)
      : null,
    resetAfter: headers.has("x-rate-limit-reset")
      ? parseInt(headers.get("x-rate-limit-reset")!, 10)
      : null,
    retryAfter: headers.has("retry-after") ? parseInt(headers.get("retry-after")!, 10) : null,
  };
}

/**
 * Format Rate Limit error message
 */
function formatRateLimitError(rateLimitInfo: RateLimitInfo): string {
  const parts: string[] = ["Figma API rate limit exceeded (429 Too Many Requests)."];

  if (rateLimitInfo.retryAfter !== null) {
    const minutes = Math.ceil(rateLimitInfo.retryAfter / 60);
    const hours = Math.ceil(rateLimitInfo.retryAfter / 3600);
    const days = Math.ceil(rateLimitInfo.retryAfter / 86400);

    if (days > 1) {
      parts.push(`Please retry after ${days} days.`);
    } else if (hours > 1) {
      parts.push(`Please retry after ${hours} hours.`);
    } else {
      parts.push(`Please retry after ${minutes} minutes.`);
    }
  }

  parts.push(
    "\nThis is likely due to Figma's November 2025 rate limit update.",
    "Starter plan: 6 requests/month. Professional plan: 10 requests/minute.",
    "\nSuggestions:",
    "1. Check if the design file belongs to a Starter plan workspace",
    "2. Duplicate the file to your own Professional workspace",
    "3. Wait for the rate limit to reset",
  );

  return parts.join(" ");
}

/**
 * Download image to local filesystem
 */
async function downloadImage(
  url: string,
  localPath: string,
  fileName: string,
  fileKey: string,
  nodeId: string,
  format: string,
): Promise<string> {
  // Validate path security
  const fullPath = validateLocalPath(localPath, fileName);

  // Check image cache
  const cachedPath = await cacheManager.hasImage(fileKey, nodeId, format);
  if (cachedPath) {
    // Copy from cache to target path
    const copied = await cacheManager.copyImageFromCache(fileKey, nodeId, format, fullPath);
    if (copied) {
      Logger.log(`Image loaded from cache: ${fileName}`);
      return fullPath;
    }
  }

  // Ensure directory exists
  const dir = path.dirname(fullPath);
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir, { recursive: true });
  }

  // Download image
  const response = await fetch(url, {
    method: "GET",
    signal: AbortSignal.timeout(30000), // 30 second timeout
  });

  if (!response.ok) {
    throw new Error(`Failed to download image: ${response.statusText}`);
  }

  // Use arrayBuffer instead of streaming for better reliability
  const buffer = await response.arrayBuffer();
  await fs.promises.writeFile(fullPath, Buffer.from(buffer));

  // Cache image
  await cacheManager.cacheImage(fullPath, fileKey, nodeId, format);

  return fullPath;
}

// ==================== Logging Utilities ====================

/**
 * Write development logs
 */
function writeLogs(name: string, value: unknown): void {
  try {
    if (process.env.NODE_ENV !== "development") return;

    const logsDir = "logs";

    try {
      fs.accessSync(process.cwd(), fs.constants.W_OK);
    } catch {
      return;
    }

    if (!fs.existsSync(logsDir)) {
      fs.mkdirSync(logsDir);
    }
    fs.writeFileSync(`${logsDir}/${name}`, JSON.stringify(value, null, 2));
  } catch {
    // Ignore log write errors
  }
}

// ==================== Figma Service Class ====================

/**
 * Figma API Service
 */
export class FigmaService {
  private readonly apiKey: string;
  private readonly baseUrl = "https://api.figma.com/v1";

  /** Most recent Rate Limit information */
  private lastRateLimitInfo: RateLimitInfo | null = null;

  constructor(apiKey: string) {
    if (!apiKey || typeof apiKey !== "string") {
      throw new Error("Figma API key is required");
    }
    this.apiKey = apiKey;
  }

  /**
   * Get most recent Rate Limit information
   */
  getRateLimitInfo(): RateLimitInfo | null {
    return this.lastRateLimitInfo;
  }

  /**
   * Make API request
   */
  private async request<T>(endpoint: string): Promise<ApiResponse<T>> {
    if (typeof fetch !== "function") {
      throw new Error(
        "The MCP server requires Node.js 18+ with fetch support.\n" +
          "Please upgrade your Node.js version to continue.",
      );
    }

    Logger.log(`Calling ${this.baseUrl}${endpoint}`);

    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      headers: {
        "X-Figma-Token": this.apiKey,
      },
    });

    // Extract Rate Limit information
    const rateLimitInfo = extractRateLimitInfo(response.headers);
    this.lastRateLimitInfo = rateLimitInfo;

    // Handle error responses
    if (!response.ok) {
      const status = response.status;
      let errorMessage = response.statusText || "Unknown error";

      // Special handling for 429 errors
      if (status === 429) {
        errorMessage = formatRateLimitError(rateLimitInfo);
      } else if (status === 403) {
        errorMessage = "Access denied. Please check your Figma API key and file permissions.";
      } else if (status === 404) {
        errorMessage = "File or node not found. Please verify the fileKey and nodeId are correct.";
      }

      throw createFigmaError(status, errorMessage, rateLimitInfo);
    }

    const data = (await response.json()) as T;
    return { data, rateLimitInfo };
  }

  /**
   * Get image fill URLs and download
   */
  async getImageFills(
    fileKey: string,
    nodes: FetchImageFillParams[],
    localPath: string,
  ): Promise<string[]> {
    if (nodes.length === 0) return [];

    // Validate parameters
    validateFileKey(fileKey);
    nodes.forEach((node) => {
      validateNodeId(node.nodeId);
    });

    const endpoint = `/files/${fileKey}/images`;
    const { data } = await this.request<GetImageFillsResponse>(endpoint);
    const { images = {} } = data.meta;

    const downloads = nodes.map(async ({ imageRef, fileName, nodeId }) => {
      const imageUrl = images[imageRef];
      if (!imageUrl) {
        Logger.log(`Image not found for ref: ${imageRef}`);
        return "";
      }

      try {
        const format = fileName.toLowerCase().endsWith(".svg") ? "svg" : "png";
        return await downloadImage(imageUrl, localPath, fileName, fileKey, nodeId, format);
      } catch (error) {
        Logger.error(`Failed to download image ${fileName}:`, error);
        return "";
      }
    });

    return Promise.all(downloads);
  }

  /**
   * Render nodes as images and download
   */
  async getImages(
    fileKey: string,
    nodes: FetchImageParams[],
    localPath: string,
  ): Promise<string[]> {
    if (nodes.length === 0) return [];

    // Validate parameters
    validateFileKey(fileKey);
    nodes.forEach((node) => validateNodeId(node.nodeId));

    // Categorize PNG and SVG nodes
    const pngNodes = nodes.filter(({ fileType }) => fileType === "png");
    const svgNodes = nodes.filter(({ fileType }) => fileType === "svg");

    // Get image URLs (sequential execution to reduce Rate Limit risk)
    const imageUrls: Record<string, string> = {};

    if (pngNodes.length > 0) {
      const pngIds = pngNodes.map(({ nodeId }) => nodeId).join(",");
      const { data } = await this.request<GetImagesResponse>(
        `/images/${fileKey}?ids=${pngIds}&scale=2&format=png`,
      );
      Object.assign(imageUrls, data.images || {});
    }

    if (svgNodes.length > 0) {
      const svgIds = svgNodes.map(({ nodeId }) => nodeId).join(",");
      const { data } = await this.request<GetImagesResponse>(
        `/images/${fileKey}?ids=${svgIds}&scale=2&format=svg`,
      );
      Object.assign(imageUrls, data.images || {});
    }

    // Download images
    const downloads = nodes.map(async ({ nodeId, fileName, fileType }) => {
      const imageUrl = imageUrls[nodeId];
      if (!imageUrl) {
        Logger.log(`Image URL not found for node: ${nodeId}`);
        return "";
      }

      try {
        return await downloadImage(imageUrl, localPath, fileName, fileKey, nodeId, fileType);
      } catch (error) {
        Logger.error(`Failed to download image ${fileName}:`, error);
        return "";
      }
    });

    return Promise.all(downloads);
  }

  /**
   * Get entire Figma file
   */
  async getFile(fileKey: string, depth?: number): Promise<SimplifiedDesign> {
    // Validate parameters
    validateFileKey(fileKey);
    validateDepth(depth);

    // Try to get from cache
    const cached = await cacheManager.getNodeData<SimplifiedDesign>(fileKey, undefined, depth);
    if (cached) {
      Logger.log(`File loaded from cache: ${fileKey}`);
      return cached;
    }

    try {
      const endpoint = `/files/${fileKey}${depth ? `?depth=${depth}` : ""}`;
      Logger.log(`Retrieving Figma file: ${fileKey} (depth: ${depth ?? "default"})`);

      const { data: response } = await this.request<GetFileResponse>(endpoint);
      Logger.log("Got response");

      const simplifiedResponse = parseFigmaResponse(response);

      // Write development logs
      writeLogs("figma-raw.json", response);
      writeLogs("figma-simplified.json", simplifiedResponse);

      // Write to cache
      await cacheManager.setNodeData(simplifiedResponse, fileKey, undefined, depth);

      return simplifiedResponse;
    } catch (error) {
      // Re-throw Figma errors to preserve details
      if ((error as FigmaError).status) {
        throw error;
      }
      Logger.error("Failed to get file:", error);
      throw error;
    }
  }

  /**
   * Get specific node
   */
  async getNode(fileKey: string, nodeId: string, depth?: number): Promise<SimplifiedDesign> {
    // Validate parameters
    validateFileKey(fileKey);
    validateNodeId(nodeId);
    validateDepth(depth);

    // Try to get from cache
    const cached = await cacheManager.getNodeData<SimplifiedDesign>(fileKey, nodeId, depth);
    if (cached) {
      Logger.log(`Node loaded from cache: ${fileKey}/${nodeId}`);
      return cached;
    }

    const endpoint = `/files/${fileKey}/nodes?ids=${nodeId}${depth ? `&depth=${depth}` : ""}`;
    const { data: response } = await this.request<GetFileNodesResponse>(endpoint);

    Logger.log("Got response from getNode, now parsing.");
    writeLogs("figma-raw.json", response);

    const simplifiedResponse = parseFigmaResponse(response);
    writeLogs("figma-simplified.json", simplifiedResponse);

    // Write to cache
    await cacheManager.setNodeData(simplifiedResponse, fileKey, nodeId, depth);

    return simplifiedResponse;
  }
}

```

--------------------------------------------------------------------------------
/src/services/cache/disk-cache.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Disk Cache
 *
 * Persistent file-based cache for Figma data and images.
 * Acts as L2 cache layer after in-memory LRU cache.
 *
 * @module services/cache/disk-cache
 */

import fs from "fs";
import path from "path";
import crypto from "crypto";
import os from "os";
import type { DiskCacheConfig, CacheEntryMeta, DiskCacheStats } from "./types.js";

/**
 * Default disk cache configuration
 */
const DEFAULT_CONFIG: DiskCacheConfig = {
  cacheDir: path.join(os.homedir(), ".figma-mcp-cache"),
  maxSize: 500 * 1024 * 1024, // 500MB
  ttl: 24 * 60 * 60 * 1000, // 24 hours
};

/**
 * Disk-based persistent cache
 */
export class DiskCache {
  private config: DiskCacheConfig;
  private dataDir: string;
  private imageDir: string;
  private metadataDir: string;
  private stats: { hits: number; misses: number };

  constructor(config: Partial<DiskCacheConfig> = {}) {
    this.config = { ...DEFAULT_CONFIG, ...config };
    this.dataDir = path.join(this.config.cacheDir, "data");
    this.imageDir = path.join(this.config.cacheDir, "images");
    this.metadataDir = path.join(this.config.cacheDir, "metadata");
    this.stats = { hits: 0, misses: 0 };

    this.ensureCacheDirectories();
  }

  /**
   * Ensure cache directories exist
   */
  private ensureCacheDirectories(): void {
    try {
      [this.config.cacheDir, this.dataDir, this.imageDir, this.metadataDir].forEach((dir) => {
        if (!fs.existsSync(dir)) {
          fs.mkdirSync(dir, { recursive: true });
        }
      });
    } catch (error) {
      console.warn("Failed to create cache directories:", error);
    }
  }

  /**
   * Generate cache key from components
   */
  static generateKey(fileKey: string, nodeId?: string, depth?: number): string {
    const keyParts = [fileKey];
    if (nodeId) keyParts.push(`node-${nodeId}`);
    if (depth !== undefined) keyParts.push(`depth-${depth}`);

    const keyString = keyParts.join("_");
    return crypto.createHash("md5").update(keyString).digest("hex");
  }

  /**
   * Generate image cache key
   */
  static generateImageKey(fileKey: string, nodeId: string, format: string): string {
    const keyString = `${fileKey}_${nodeId}_${format}`;
    return crypto.createHash("md5").update(keyString).digest("hex");
  }

  // ==================== Node Data Operations ====================

  /**
   * Get cached node data
   */
  async get<T>(
    fileKey: string,
    nodeId?: string,
    depth?: number,
    version?: string,
  ): Promise<T | null> {
    try {
      const cacheKey = DiskCache.generateKey(fileKey, nodeId, depth);
      const dataPath = path.join(this.dataDir, `${cacheKey}.json`);
      const metadataPath = path.join(this.metadataDir, `${cacheKey}.meta.json`);

      // Check if files exist
      if (!fs.existsSync(dataPath) || !fs.existsSync(metadataPath)) {
        this.stats.misses++;
        return null;
      }

      // Read metadata and check expiration
      const metadata: CacheEntryMeta = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));

      // Check TTL expiration
      if (Date.now() > metadata.expiresAt) {
        this.deleteByKey(cacheKey);
        this.stats.misses++;
        return null;
      }

      // Check version mismatch
      if (version && metadata.version && metadata.version !== version) {
        this.deleteByKey(cacheKey);
        this.stats.misses++;
        return null;
      }

      // Read and return cached data
      const data = JSON.parse(fs.readFileSync(dataPath, "utf-8"));
      this.stats.hits++;
      return data as T;
    } catch (error) {
      console.warn("Failed to read disk cache:", error);
      this.stats.misses++;
      return null;
    }
  }

  /**
   * Set node data cache
   */
  async set<T>(
    data: T,
    fileKey: string,
    nodeId?: string,
    depth?: number,
    version?: string,
  ): Promise<void> {
    try {
      const cacheKey = DiskCache.generateKey(fileKey, nodeId, depth);
      const dataPath = path.join(this.dataDir, `${cacheKey}.json`);
      const metadataPath = path.join(this.metadataDir, `${cacheKey}.meta.json`);

      const dataString = JSON.stringify(data, null, 2);

      // Create metadata
      const metadata: CacheEntryMeta = {
        key: cacheKey,
        createdAt: Date.now(),
        expiresAt: Date.now() + this.config.ttl,
        fileKey,
        nodeId,
        depth,
        version,
        size: Buffer.byteLength(dataString, "utf-8"),
      };

      // Write data and metadata
      fs.writeFileSync(dataPath, dataString);
      fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));

      // Enforce size limit asynchronously
      this.enforceSizeLimit().catch(() => {});
    } catch (error) {
      console.warn("Failed to write disk cache:", error);
    }
  }

  /**
   * Check if cache entry exists
   */
  async has(fileKey: string, nodeId?: string, depth?: number): Promise<boolean> {
    const cacheKey = DiskCache.generateKey(fileKey, nodeId, depth);
    const dataPath = path.join(this.dataDir, `${cacheKey}.json`);
    const metadataPath = path.join(this.metadataDir, `${cacheKey}.meta.json`);

    if (!fs.existsSync(dataPath) || !fs.existsSync(metadataPath)) {
      return false;
    }

    try {
      const metadata: CacheEntryMeta = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
      return Date.now() <= metadata.expiresAt;
    } catch {
      return false;
    }
  }

  /**
   * Delete cache entry by key
   */
  private deleteByKey(cacheKey: string): void {
    try {
      const dataPath = path.join(this.dataDir, `${cacheKey}.json`);
      const metadataPath = path.join(this.metadataDir, `${cacheKey}.meta.json`);

      if (fs.existsSync(dataPath)) fs.unlinkSync(dataPath);
      if (fs.existsSync(metadataPath)) fs.unlinkSync(metadataPath);
    } catch {
      // Ignore deletion errors
    }
  }

  /**
   * Delete cache for a file key
   */
  async delete(fileKey: string, nodeId?: string, depth?: number): Promise<boolean> {
    const cacheKey = DiskCache.generateKey(fileKey, nodeId, depth);
    this.deleteByKey(cacheKey);
    return true;
  }

  /**
   * Invalidate all cache entries for a file
   */
  async invalidateFile(fileKey: string): Promise<number> {
    let invalidated = 0;

    try {
      const metadataFiles = fs.readdirSync(this.metadataDir);

      for (const file of metadataFiles) {
        if (!file.endsWith(".meta.json") || file.startsWith("img_")) continue;

        const metadataPath = path.join(this.metadataDir, file);
        try {
          const metadata: CacheEntryMeta = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));

          if (metadata.fileKey === fileKey) {
            const cacheKey = file.replace(".meta.json", "");
            this.deleteByKey(cacheKey);
            invalidated++;
          }
        } catch {
          // Skip individual file errors
        }
      }
    } catch (error) {
      console.warn("Failed to invalidate file cache:", error);
    }

    return invalidated;
  }

  // ==================== Image Operations ====================

  /**
   * Check if image is cached
   */
  async hasImage(fileKey: string, nodeId: string, format: string): Promise<string | null> {
    try {
      const cacheKey = DiskCache.generateImageKey(fileKey, nodeId, format);
      const imagePath = path.join(this.imageDir, `${cacheKey}.${format.toLowerCase()}`);
      const metadataPath = path.join(this.metadataDir, `img_${cacheKey}.meta.json`);

      if (!fs.existsSync(imagePath) || !fs.existsSync(metadataPath)) {
        return null;
      }

      // Check expiration
      const metadata: CacheEntryMeta = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
      if (Date.now() > metadata.expiresAt) {
        this.deleteImageByKey(cacheKey, format);
        return null;
      }

      return imagePath;
    } catch {
      return null;
    }
  }

  /**
   * Cache image file
   */
  async cacheImage(
    sourcePath: string,
    fileKey: string,
    nodeId: string,
    format: string,
  ): Promise<string> {
    try {
      const cacheKey = DiskCache.generateImageKey(fileKey, nodeId, format);
      const cachedImagePath = path.join(this.imageDir, `${cacheKey}.${format.toLowerCase()}`);
      const metadataPath = path.join(this.metadataDir, `img_${cacheKey}.meta.json`);

      // Copy image to cache
      fs.copyFileSync(sourcePath, cachedImagePath);

      // Get file size
      const stats = fs.statSync(cachedImagePath);

      // Create metadata
      const metadata: CacheEntryMeta = {
        key: cacheKey,
        createdAt: Date.now(),
        expiresAt: Date.now() + this.config.ttl,
        fileKey,
        nodeId,
        size: stats.size,
      };
      fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));

      return cachedImagePath;
    } catch (error) {
      console.warn("Failed to cache image:", error);
      return sourcePath;
    }
  }

  /**
   * Copy image from cache to target path
   */
  async copyImageFromCache(
    fileKey: string,
    nodeId: string,
    format: string,
    targetPath: string,
  ): Promise<boolean> {
    const cachedPath = await this.hasImage(fileKey, nodeId, format);
    if (!cachedPath) return false;

    try {
      const targetDir = path.dirname(targetPath);
      if (!fs.existsSync(targetDir)) {
        fs.mkdirSync(targetDir, { recursive: true });
      }

      fs.copyFileSync(cachedPath, targetPath);
      return true;
    } catch {
      return false;
    }
  }

  /**
   * Delete image cache by key
   */
  private deleteImageByKey(cacheKey: string, format: string): void {
    try {
      const imagePath = path.join(this.imageDir, `${cacheKey}.${format.toLowerCase()}`);
      const metadataPath = path.join(this.metadataDir, `img_${cacheKey}.meta.json`);

      if (fs.existsSync(imagePath)) fs.unlinkSync(imagePath);
      if (fs.existsSync(metadataPath)) fs.unlinkSync(metadataPath);
    } catch {
      // Ignore errors
    }
  }

  // ==================== Maintenance Operations ====================

  /**
   * Clean all expired cache entries
   */
  async cleanExpired(): Promise<number> {
    let deletedCount = 0;
    const now = Date.now();

    try {
      const metadataFiles = fs.readdirSync(this.metadataDir);

      for (const file of metadataFiles) {
        if (!file.endsWith(".meta.json")) continue;

        const metadataPath = path.join(this.metadataDir, file);
        try {
          const metadata: CacheEntryMeta = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));

          if (now > metadata.expiresAt) {
            const cacheKey = file.replace(".meta.json", "");

            if (file.startsWith("img_")) {
              // Image cache
              const imgCacheKey = cacheKey.replace("img_", "");
              ["png", "jpg", "svg", "pdf"].forEach((format) => {
                const imagePath = path.join(this.imageDir, `${imgCacheKey}.${format}`);
                if (fs.existsSync(imagePath)) fs.unlinkSync(imagePath);
              });
            } else {
              // Data cache
              const dataPath = path.join(this.dataDir, `${cacheKey}.json`);
              if (fs.existsSync(dataPath)) fs.unlinkSync(dataPath);
            }

            fs.unlinkSync(metadataPath);
            deletedCount++;
          }
        } catch {
          // Skip individual errors
        }
      }
    } catch (error) {
      console.warn("Failed to clean expired cache:", error);
    }

    return deletedCount;
  }

  /**
   * Enforce size limit by removing oldest entries
   */
  async enforceSizeLimit(): Promise<number> {
    let removedCount = 0;

    try {
      const stats = await this.getStats();
      if (stats.totalSize <= this.config.maxSize) {
        return 0;
      }

      // Get all metadata entries sorted by creation time
      const entries: Array<{ path: string; meta: CacheEntryMeta }> = [];
      const metadataFiles = fs.readdirSync(this.metadataDir);

      for (const file of metadataFiles) {
        if (!file.endsWith(".meta.json")) continue;

        const metadataPath = path.join(this.metadataDir, file);
        try {
          const metadata: CacheEntryMeta = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
          entries.push({ path: metadataPath, meta: metadata });
        } catch {
          // Skip invalid entries
        }
      }

      // Sort by creation time (oldest first)
      entries.sort((a, b) => a.meta.createdAt - b.meta.createdAt);

      // Remove oldest entries until under limit
      let currentSize = stats.totalSize;
      for (const entry of entries) {
        if (currentSize <= this.config.maxSize) break;

        const cacheKey = path.basename(entry.path).replace(".meta.json", "");

        if (cacheKey.startsWith("img_")) {
          const imgKey = cacheKey.replace("img_", "");
          ["png", "jpg", "svg", "pdf"].forEach((format) => {
            const imagePath = path.join(this.imageDir, `${imgKey}.${format}`);
            if (fs.existsSync(imagePath)) {
              currentSize -= fs.statSync(imagePath).size;
              fs.unlinkSync(imagePath);
            }
          });
        } else {
          const dataPath = path.join(this.dataDir, `${cacheKey}.json`);
          if (fs.existsSync(dataPath)) {
            currentSize -= fs.statSync(dataPath).size;
            fs.unlinkSync(dataPath);
          }
        }

        fs.unlinkSync(entry.path);
        currentSize -= entry.meta.size || 0;
        removedCount++;
      }
    } catch (error) {
      console.warn("Failed to enforce size limit:", error);
    }

    return removedCount;
  }

  /**
   * Clear all cache
   */
  async clearAll(): Promise<void> {
    try {
      [this.dataDir, this.imageDir, this.metadataDir].forEach((dir) => {
        if (fs.existsSync(dir)) {
          const files = fs.readdirSync(dir);
          files.forEach((file) => {
            fs.unlinkSync(path.join(dir, file));
          });
        }
      });
      this.stats = { hits: 0, misses: 0 };
    } catch (error) {
      console.warn("Failed to clear cache:", error);
    }
  }

  /**
   * Get cache statistics
   */
  async getStats(): Promise<DiskCacheStats> {
    let totalSize = 0;
    let nodeFileCount = 0;
    let imageFileCount = 0;

    try {
      if (fs.existsSync(this.dataDir)) {
        const dataFiles = fs.readdirSync(this.dataDir);
        nodeFileCount = dataFiles.filter((f) => f.endsWith(".json")).length;
        dataFiles.forEach((file) => {
          const stat = fs.statSync(path.join(this.dataDir, file));
          totalSize += stat.size;
        });
      }

      if (fs.existsSync(this.imageDir)) {
        const imageFiles = fs.readdirSync(this.imageDir);
        imageFileCount = imageFiles.length;
        imageFiles.forEach((file) => {
          const stat = fs.statSync(path.join(this.imageDir, file));
          totalSize += stat.size;
        });
      }
    } catch {
      // Ignore errors
    }

    return {
      hits: this.stats.hits,
      misses: this.stats.misses,
      totalSize,
      maxSize: this.config.maxSize,
      nodeFileCount,
      imageFileCount,
    };
  }

  /**
   * Get cache directory path
   */
  getCacheDir(): string {
    return this.config.cacheDir;
  }
}

```

--------------------------------------------------------------------------------
/src/algorithms/icon/detector.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Icon Detection Algorithm
 *
 * Industry-based algorithm for detecting icons and mergeable layer groups.
 *
 * Core strategies:
 * 1. Prioritize Figma exportSettings (designer-marked exports)
 * 2. Smart detection: based on size, type ratio, structure depth
 * 3. Bottom-up merging: child icon groups merge first, then parent nodes
 *
 * @module algorithms/icon/detector
 */

import type { IconDetectionResult, IconDetectionConfig } from "~/types/index.js";

// Re-export types for module consumers
export type { IconDetectionResult };

// Use IconDetectionConfig from types, alias as DetectionConfig for internal use
export type DetectionConfig = IconDetectionConfig;

// ==================== Module-Specific Types ====================

/**
 * Figma node structure for icon detection (minimal interface)
 */
export interface FigmaNode {
  id: string;
  name: string;
  type: string;
  children?: FigmaNode[];
  absoluteBoundingBox?: {
    x: number;
    y: number;
    width: number;
    height: number;
  };
  exportSettings?: Array<{
    format: string;
    suffix?: string;
    constraint?: {
      type: string;
      value: number;
    };
  }>;
  fills?: Array<{
    type: string;
    visible?: boolean;
    imageRef?: string;
    blendMode?: string;
  }>;
  effects?: Array<{
    type: string;
    visible?: boolean;
  }>;
  strokes?: Array<unknown>;
}

// ==================== Constants ====================

/** Default detection configuration */
export const DEFAULT_CONFIG: DetectionConfig = {
  maxIconSize: 300,
  minIconSize: 8,
  mergeableRatio: 0.6,
  maxDepth: 5,
  maxChildren: 100,
  respectExportSettingsMaxSize: 400,
};

/** Container node types */
const CONTAINER_TYPES = ["GROUP", "FRAME", "COMPONENT", "INSTANCE"] as const;

/** Mergeable graphics types (can be represented as SVG) */
const MERGEABLE_TYPES = [
  "VECTOR",
  "RECTANGLE",
  "ELLIPSE",
  "LINE",
  "POLYGON",
  "STAR",
  "BOOLEAN_OPERATION",
  "REGULAR_POLYGON",
] as const;

/** Single element types that should not be auto-exported (typically backgrounds) */
const SINGLE_ELEMENT_EXCLUDE_TYPES = ["RECTANGLE"] as const;

/** Types that exclude a group from being merged as icon */
const EXCLUDE_TYPES = ["TEXT", "COMPONENT", "INSTANCE"] as const;

/** Effects that require PNG export */
const PNG_REQUIRED_EFFECTS = [
  "DROP_SHADOW",
  "INNER_SHADOW",
  "LAYER_BLUR",
  "BACKGROUND_BLUR",
] as const;

// ==================== Helper Functions ====================

/**
 * Check if type is a container type
 */
function isContainerType(type: string): boolean {
  return CONTAINER_TYPES.includes(type as (typeof CONTAINER_TYPES)[number]);
}

/**
 * Check if type is mergeable (can be part of an icon)
 */
function isMergeableType(type: string): boolean {
  return MERGEABLE_TYPES.includes(type as (typeof MERGEABLE_TYPES)[number]);
}

/**
 * Check if type should be excluded from icon merging
 */
function isExcludeType(type: string): boolean {
  return EXCLUDE_TYPES.includes(type as (typeof EXCLUDE_TYPES)[number]);
}

/**
 * Get node dimensions
 */
function getNodeSize(node: FigmaNode): { width: number; height: number } | null {
  if (!node.absoluteBoundingBox) return null;
  return {
    width: node.absoluteBoundingBox.width,
    height: node.absoluteBoundingBox.height,
  };
}

/**
 * Check if node has image fill
 */
function hasImageFill(node: FigmaNode): boolean {
  if (!node.fills) return false;
  return node.fills.some(
    (fill) => fill.type === "IMAGE" && fill.visible !== false && fill.imageRef,
  );
}

/**
 * Check if node has complex effects (requires PNG)
 */
function hasComplexEffects(node: FigmaNode): boolean {
  if (!node.effects) return false;
  return node.effects.some(
    (effect) =>
      effect.visible !== false &&
      PNG_REQUIRED_EFFECTS.includes(effect.type as (typeof PNG_REQUIRED_EFFECTS)[number]),
  );
}

// ==================== Optimized Single-Pass Stats Collection ====================

/**
 * Statistics collected from a node tree in a single traversal
 * This replaces multiple recursive functions with one unified pass
 */
interface NodeTreeStats {
  /** Maximum depth of the tree */
  depth: number;
  /** Total number of descendants (not including root) */
  totalChildren: number;
  /** Whether tree contains excluded types (TEXT, COMPONENT, INSTANCE) */
  hasExcludeType: boolean;
  /** Whether tree contains image fills */
  hasImageFill: boolean;
  /** Whether tree contains complex effects requiring PNG */
  hasComplexEffects: boolean;
  /** Whether all leaf nodes are mergeable types */
  allLeavesMergeable: boolean;
  /** Ratio of mergeable types in direct children */
  mergeableRatio: number;
}

/**
 * Collect all tree statistics in a single traversal
 *
 * OPTIMIZATION: This replaces 6 separate recursive functions:
 * - calculateDepth()
 * - countTotalChildren()
 * - hasExcludeTypeInTree()
 * - hasImageFillInTree()
 * - hasComplexEffectsInTree()
 * - areAllLeavesMergeable()
 *
 * Before: O(6n) - each function traverses entire tree
 * After: O(n) - single traversal collects all data
 *
 * @param node - Node to analyze
 * @returns Collected statistics
 */
function collectNodeStats(node: FigmaNode): NodeTreeStats {
  // Base case: leaf node (no children)
  if (!node.children || node.children.length === 0) {
    const isMergeable = isMergeableType(node.type);
    return {
      depth: 0,
      totalChildren: 0,
      hasExcludeType: isExcludeType(node.type),
      hasImageFill: hasImageFill(node),
      hasComplexEffects: hasComplexEffects(node),
      allLeavesMergeable: isMergeable,
      mergeableRatio: isMergeable ? 1 : 0,
    };
  }

  // Recursive case: collect stats from all children
  const childStats = node.children.map(collectNodeStats);

  // Aggregate child statistics
  const maxChildDepth = Math.max(...childStats.map((s) => s.depth));
  const totalDescendants = childStats.reduce((sum, s) => sum + 1 + s.totalChildren, 0);
  const hasExcludeInChildren = childStats.some((s) => s.hasExcludeType);
  const hasImageInChildren = childStats.some((s) => s.hasImageFill);
  const hasEffectsInChildren = childStats.some((s) => s.hasComplexEffects);
  const allChildrenMergeable = childStats.every((s) => s.allLeavesMergeable);

  // Calculate mergeable ratio for direct children
  const mergeableCount = node.children.filter(
    (child) => isMergeableType(child.type) || isContainerType(child.type),
  ).length;
  const mergeableRatio = mergeableCount / node.children.length;

  // Determine if all leaves are mergeable
  // For containers: all children must have all leaves mergeable
  // For other types: check if this type itself is mergeable
  const allLeavesMergeable = isContainerType(node.type)
    ? allChildrenMergeable
    : isMergeableType(node.type);

  return {
    depth: maxChildDepth + 1,
    totalChildren: totalDescendants,
    hasExcludeType: isExcludeType(node.type) || hasExcludeInChildren,
    hasImageFill: hasImageFill(node) || hasImageInChildren,
    hasComplexEffects: hasComplexEffects(node) || hasEffectsInChildren,
    allLeavesMergeable,
    mergeableRatio,
  };
}

// ==================== Main Detection Functions ====================

/**
 * Detect if a single node should be exported as an icon
 *
 * OPTIMIZED: Uses single-pass collectNodeStats() instead of multiple recursive functions
 *
 * @param node - Figma node to analyze
 * @param config - Detection configuration
 * @returns Detection result with export recommendation
 */
export function detectIcon(
  node: FigmaNode,
  config: DetectionConfig = DEFAULT_CONFIG,
): IconDetectionResult {
  const result: IconDetectionResult = {
    nodeId: node.id,
    nodeName: node.name,
    shouldMerge: false,
    exportFormat: "SVG",
    reason: "",
  };

  // Get node size once
  const size = getNodeSize(node);
  if (size) {
    result.size = size;
  }

  // 1. Check Figma exportSettings (with size restrictions)
  if (node.exportSettings && node.exportSettings.length > 0) {
    const isSmallEnough =
      !size ||
      (size.width <= config.respectExportSettingsMaxSize &&
        size.height <= config.respectExportSettingsMaxSize);

    // For exportSettings, we need to check for excluded types
    // Use optimized single-pass collection
    const stats = collectNodeStats(node);
    const containsText = stats.hasExcludeType;

    if (isSmallEnough && !containsText) {
      const exportSetting = node.exportSettings[0];
      result.shouldMerge = true;
      result.exportFormat = exportSetting.format === "SVG" ? "SVG" : "PNG";
      result.reason = `Designer marked export as ${exportSetting.format}`;
      return result;
    }
    // Large nodes or nodes with TEXT: ignore exportSettings, continue detection
  }

  // 2. Must be container type or mergeable single element
  if (!isContainerType(node.type)) {
    // Single mergeable type node
    if (isMergeableType(node.type)) {
      // Single RECTANGLE is typically a background, not exported
      if (
        SINGLE_ELEMENT_EXCLUDE_TYPES.includes(
          node.type as (typeof SINGLE_ELEMENT_EXCLUDE_TYPES)[number],
        )
      ) {
        result.reason = `Single ${node.type} is typically a background, not exported`;
        return result;
      }

      // Check size for single elements
      if (size) {
        if (size.width > config.maxIconSize || size.height > config.maxIconSize) {
          result.reason = `Single element too large (${Math.round(size.width)}x${Math.round(size.height)} > ${config.maxIconSize})`;
          return result;
        }
      }
      result.shouldMerge = true;
      result.exportFormat = hasComplexEffects(node) ? "PNG" : "SVG";
      result.reason = "Single vector/shape element";
      return result;
    }
    result.reason = "Not a container or mergeable type";
    return result;
  }

  // 3. Check size
  if (size) {
    // Too large: likely a layout container
    if (size.width > config.maxIconSize || size.height > config.maxIconSize) {
      result.reason = `Size too large (${size.width}x${size.height} > ${config.maxIconSize})`;
      return result;
    }

    // Too small
    if (size.width < config.minIconSize && size.height < config.minIconSize) {
      result.reason = `Size too small (${size.width}x${size.height} < ${config.minIconSize})`;
      return result;
    }
  }

  // OPTIMIZATION: Collect all tree statistics in a single pass
  // This replaces 6 separate recursive traversals with 1
  const stats = collectNodeStats(node);

  // 4. Check for excluded types (TEXT, etc.)
  if (stats.hasExcludeType) {
    result.reason = "Contains TEXT or other exclude types";
    return result;
  }

  // 5. Check structure depth
  if (stats.depth > config.maxDepth) {
    result.reason = `Depth too deep (${stats.depth} > ${config.maxDepth})`;
    return result;
  }

  // 6. Check child count
  result.childCount = stats.totalChildren;
  if (stats.totalChildren > config.maxChildren) {
    result.reason = `Too many children (${stats.totalChildren} > ${config.maxChildren})`;
    return result;
  }

  // 7. Check mergeable type ratio
  if (stats.mergeableRatio < config.mergeableRatio) {
    result.reason = `Mergeable ratio too low (${(stats.mergeableRatio * 100).toFixed(1)}% < ${config.mergeableRatio * 100}%)`;
    return result;
  }

  // 8. Check if all leaf nodes are mergeable
  if (!stats.allLeavesMergeable) {
    result.reason = "Not all leaf nodes are mergeable types";
    return result;
  }

  // 9. Determine export format (using stats collected in single pass)
  if (stats.hasImageFill) {
    result.exportFormat = "PNG";
    result.reason = "Contains image fills, export as PNG";
  } else if (stats.hasComplexEffects) {
    result.exportFormat = "PNG";
    result.reason = "Contains complex effects, export as PNG";
  } else {
    result.exportFormat = "SVG";
    result.reason = "All vector elements, export as SVG";
  }

  result.shouldMerge = true;
  return result;
}

/**
 * Process node tree bottom-up, detecting and marking icons
 *
 * @param node - Root node
 * @param config - Detection configuration
 * @returns Processed node with _iconDetection markers
 */
export function processNodeTree(
  node: FigmaNode,
  config: DetectionConfig = DEFAULT_CONFIG,
): FigmaNode & { _iconDetection?: IconDetectionResult } {
  const processedNode = { ...node } as FigmaNode & { _iconDetection?: IconDetectionResult };

  // Process children first (bottom-up)
  if (node.children && node.children.length > 0) {
    processedNode.children = node.children.map((child) => processNodeTree(child, config));

    // Check if all children are marked as icons (can be merged to parent)
    const allChildrenAreIcons = processedNode.children.every((child) => {
      const childWithDetection = child as FigmaNode & { _iconDetection?: IconDetectionResult };
      return childWithDetection._iconDetection?.shouldMerge;
    });

    // If all children are icons, try to merge to current node
    if (allChildrenAreIcons) {
      const detection = detectIcon(processedNode, config);
      if (detection.shouldMerge) {
        processedNode._iconDetection = detection;
        // Clear child markers since they will be merged
        processedNode.children.forEach((child) => {
          delete (child as FigmaNode & { _iconDetection?: IconDetectionResult })._iconDetection;
        });
        return processedNode;
      }
    }
  }

  // Detect current node
  const detection = detectIcon(processedNode, config);
  if (detection.shouldMerge) {
    processedNode._iconDetection = detection;
  }

  return processedNode;
}

/**
 * Collect all exportable icons from processed node tree
 *
 * @param node - Processed node with _iconDetection markers
 * @returns Array of icon detection results
 */
export function collectExportableIcons(
  node: FigmaNode & { _iconDetection?: IconDetectionResult },
): IconDetectionResult[] {
  const results: IconDetectionResult[] = [];

  // If current node is an icon, add to results
  if (node._iconDetection?.shouldMerge) {
    results.push(node._iconDetection);
    // Don't recurse into children (they will be merged)
    return results;
  }

  // Recurse into children
  if (node.children) {
    for (const child of node.children) {
      results.push(
        ...collectExportableIcons(child as FigmaNode & { _iconDetection?: IconDetectionResult }),
      );
    }
  }

  return results;
}

/**
 * Analyze node tree and return icon detection report
 *
 * @param node - Root Figma node
 * @param config - Detection configuration
 * @returns Analysis result with processed tree, exportable icons, and summary
 */
export function analyzeNodeTree(
  node: FigmaNode,
  config: DetectionConfig = DEFAULT_CONFIG,
): {
  processedTree: FigmaNode & { _iconDetection?: IconDetectionResult };
  exportableIcons: IconDetectionResult[];
  summary: {
    totalIcons: number;
    svgCount: number;
    pngCount: number;
  };
} {
  const processedTree = processNodeTree(node, config);
  const exportableIcons = collectExportableIcons(processedTree);

  const summary = {
    totalIcons: exportableIcons.length,
    svgCount: exportableIcons.filter((i) => i.exportFormat === "SVG").length,
    pngCount: exportableIcons.filter((i) => i.exportFormat === "PNG").length,
  };

  return { processedTree, exportableIcons, summary };
}

```

--------------------------------------------------------------------------------
/tests/integration/layout-optimization.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Layout Optimization Integration Tests
 *
 * Tests the complete layout optimization pipeline using real Figma data.
 * Converted from scripts/test-layout-optimization.ts
 */

import { describe, it, expect, beforeAll } from "vitest";
import * as fs from "fs";
import * as path from "path";
import { fileURLToPath } from "url";
import { parseFigmaResponse } from "~/core/parser.js";
import { LayoutOptimizer } from "~/algorithms/layout/optimizer.js";
import type { SimplifiedNode, SimplifiedDesign } from "~/types/index.js";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const fixturesDir = path.join(__dirname, "../fixtures/figma-data");
const expectedDir = path.join(__dirname, "../fixtures/expected");

// Test file configurations
const TEST_FILES = [
  { name: "node-402-34955", desc: "Group 1410104853 (1580x895)" },
  { name: "node-240-32163", desc: "Call Logs 有数据 (375x827)" },
];

// Layout statistics interface
interface LayoutStats {
  total: number;
  flex: number;
  grid: number;
  flexRow: number;
  flexColumn: number;
  absolute: number;
}

// Helper: Count layouts recursively
function countLayouts(node: SimplifiedNode, stats: LayoutStats): void {
  stats.total++;

  const display = node.cssStyles?.display;
  if (display === "flex") {
    stats.flex++;
    if (node.cssStyles?.flexDirection === "column") {
      stats.flexColumn++;
    } else {
      stats.flexRow++;
    }
  } else if (display === "grid") {
    stats.grid++;
  } else if (node.cssStyles?.position === "absolute") {
    stats.absolute++;
  }

  if (node.children) {
    for (const child of node.children) {
      countLayouts(child, stats);
    }
  }
}

// Helper: Find nodes by layout type
function findLayoutNodes(
  node: SimplifiedNode,
  layoutType: "flex" | "grid",
  results: SimplifiedNode[] = [],
): SimplifiedNode[] {
  if (node.cssStyles?.display === layoutType) {
    results.push(node);
  }

  if (node.children) {
    for (const child of node.children) {
      findLayoutNodes(child, layoutType, results);
    }
  }

  return results;
}

// Helper: Load raw fixture data
function loadFixture(name: string): unknown {
  const filePath = path.join(fixturesDir, `${name}.json`);
  return JSON.parse(fs.readFileSync(filePath, "utf-8"));
}

// Helper: Load expected output if exists
function loadExpectedOutput(name: string): unknown | null {
  const filePath = path.join(expectedDir, `${name}-optimized.json`);
  if (fs.existsSync(filePath)) {
    return JSON.parse(fs.readFileSync(filePath, "utf-8"));
  }
  return null;
}

describe("Layout Optimization Integration", () => {
  TEST_FILES.forEach(({ name, desc }) => {
    describe(`${name} (${desc})`, () => {
      let rawData: unknown;
      let result: ReturnType<typeof parseFigmaResponse>;
      let originalSize: number;
      let optimizedSize: number;
      let stats: LayoutStats;

      beforeAll(() => {
        const filePath = path.join(fixturesDir, `${name}.json`);
        if (!fs.existsSync(filePath)) {
          throw new Error(`Test fixture not found: ${name}.json`);
        }

        rawData = loadFixture(name);
        originalSize = fs.statSync(filePath).size;

        result = parseFigmaResponse(rawData as Parameters<typeof parseFigmaResponse>[0]);
        optimizedSize = Buffer.byteLength(JSON.stringify(result));

        // Calculate layout stats
        stats = {
          total: 0,
          flex: 0,
          grid: 0,
          flexRow: 0,
          flexColumn: 0,
          absolute: 0,
        };
        for (const node of result.nodes) {
          countLayouts(node, stats);
        }
      });

      describe("Data Compression", () => {
        it("should achieve significant data compression", () => {
          const compressionRate = (1 - optimizedSize / originalSize) * 100;
          // Should achieve at least 50% compression
          expect(compressionRate).toBeGreaterThan(50);
        });

        it("should reduce file size to under 100KB for typical nodes", () => {
          // Optimized output should be reasonable size
          expect(optimizedSize).toBeLessThan(100 * 1024);
        });
      });

      describe("Layout Detection", () => {
        it("should detect layout types (not all absolute)", () => {
          // Should have some flex or grid layouts
          expect(stats.flex + stats.grid).toBeGreaterThan(0);
        });

        it("should not have excessive absolute positioning", () => {
          // Absolute positioning should not dominate in most cases
          // Some fixtures may have higher absolute ratios due to their design
          const absoluteRatio = stats.absolute / stats.total;
          // Allow up to 90% for complex layouts, but ensure some semantic layouts exist
          expect(absoluteRatio).toBeLessThan(0.9);
        });

        it("should have balanced flex row/column distribution", () => {
          // At least some flex should be detected
          expect(stats.flex).toBeGreaterThan(0);
        });
      });

      describe("Flex Layout Properties", () => {
        it("should have valid flex properties when flex is detected", () => {
          const flexNodes = result.nodes.flatMap((n) => findLayoutNodes(n, "flex"));

          flexNodes.forEach((node) => {
            expect(node.cssStyles?.display).toBe("flex");
            // Direction should be defined
            expect(["row", "column", undefined]).toContain(node.cssStyles?.flexDirection);
          });
        });

        it("should have gap property for flex containers with spacing", () => {
          const flexNodes = result.nodes.flatMap((n) => findLayoutNodes(n, "flex"));

          // At least some flex containers should have gap
          const flexWithGap = flexNodes.filter((n) => n.cssStyles?.gap);
          if (flexNodes.length > 2) {
            expect(flexWithGap.length).toBeGreaterThan(0);
          }
        });
      });

      describe("Grid Layout Properties", () => {
        it("should have valid grid properties when grid is detected", () => {
          const gridNodes = result.nodes.flatMap((n) => findLayoutNodes(n, "grid"));

          gridNodes.forEach((node) => {
            expect(node.cssStyles?.display).toBe("grid");
            // Grid must have gridTemplateColumns
            expect(node.cssStyles?.gridTemplateColumns).toBeDefined();
          });
        });

        it("should have at least 4 children for grid containers", () => {
          const gridNodes = result.nodes.flatMap((n) => findLayoutNodes(n, "grid"));

          gridNodes.forEach((node) => {
            expect(node.children?.length).toBeGreaterThanOrEqual(4);
          });
        });

        it("should have valid gridTemplateColumns format", () => {
          const gridNodes = result.nodes.flatMap((n) => findLayoutNodes(n, "grid"));

          gridNodes.forEach((node) => {
            const columns = node.cssStyles?.gridTemplateColumns;
            if (columns) {
              // Should be space-separated pixel values or repeat()
              expect(columns).toMatch(/^(\d+px\s*)+$|^repeat\(/);
            }
          });
        });
      });

      describe("Child Style Cleanup", () => {
        it("should clean position:absolute from flex children", () => {
          const flexNodes = result.nodes.flatMap((n) => findLayoutNodes(n, "flex"));

          flexNodes.forEach((parent) => {
            parent.children?.forEach((child) => {
              // Non-overlapping children should not have position:absolute
              if (!hasOverlapWithSiblings(child, parent.children || [])) {
                expect(child.cssStyles?.position).not.toBe("absolute");
              }
            });
          });
        });

        it("should clean left/top from flex children", () => {
          const flexNodes = result.nodes.flatMap((n) => findLayoutNodes(n, "flex"));

          flexNodes.forEach((parent) => {
            parent.children?.forEach((child) => {
              if (!hasOverlapWithSiblings(child, parent.children || [])) {
                expect(child.cssStyles?.left).toBeUndefined();
                expect(child.cssStyles?.top).toBeUndefined();
              }
            });
          });
        });
      });

      describe("Output Structure", () => {
        it("should have correct response structure", () => {
          expect(result).toHaveProperty("name");
          expect(result).toHaveProperty("nodes");
          expect(Array.isArray(result.nodes)).toBe(true);
        });

        it("should preserve node hierarchy", () => {
          expect(result.nodes.length).toBeGreaterThan(0);
          const rootNode = result.nodes[0];
          expect(rootNode).toHaveProperty("id");
          expect(rootNode).toHaveProperty("name");
          expect(rootNode).toHaveProperty("type");
        });
      });

      describe("Snapshot Comparison", () => {
        it("should match expected output structure", () => {
          const expected = loadExpectedOutput(name);
          if (expected) {
            // Compare node count
            expect(result.nodes.length).toBe((expected as { nodes: unknown[] }).nodes.length);
          }
        });

        it("should produce consistent layout stats", () => {
          // Use inline snapshot for layout stats
          expect({
            total: stats.total,
            flexRatio: Math.round((stats.flex / stats.total) * 100),
            gridRatio: Math.round((stats.grid / stats.total) * 100),
            absoluteRatio: Math.round((stats.absolute / stats.total) * 100),
          }).toMatchSnapshot();
        });
      });
    });
  });
});

// Helper: Check if a node overlaps with its siblings
function hasOverlapWithSiblings(node: SimplifiedNode, siblings: SimplifiedNode[]): boolean {
  const nodeRect = extractRect(node);
  if (!nodeRect) return false;

  return siblings.some((sibling) => {
    if (sibling.id === node.id) return false;
    const siblingRect = extractRect(sibling);
    if (!siblingRect) return false;

    return calculateIoU(nodeRect, siblingRect) > 0.1;
  });
}

interface Rect {
  x: number;
  y: number;
  width: number;
  height: number;
}

function extractRect(node: SimplifiedNode): Rect | null {
  const styles = node.cssStyles;
  if (!styles?.width || !styles?.height) return null;

  return {
    x: parseFloat(styles.left || "0"),
    y: parseFloat(styles.top || "0"),
    width: parseFloat(styles.width),
    height: parseFloat(styles.height),
  };
}

function calculateIoU(a: Rect, b: Rect): number {
  const xOverlap = Math.max(0, Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x));
  const yOverlap = Math.max(0, Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y));
  const intersection = xOverlap * yOverlap;

  const areaA = a.width * a.height;
  const areaB = b.width * b.height;
  const union = areaA + areaB - intersection;

  return union > 0 ? intersection / union : 0;
}

// ==================== Optimization Idempotency Tests ====================

describe("Layout Optimization Idempotency", () => {
  // Helper: Count occurrences of a key-value pair in object tree
  function countOccurrences(obj: unknown, key: string, value: string): number {
    let count = 0;
    function traverse(o: unknown): void {
      if (o && typeof o === "object") {
        if (Array.isArray(o)) {
          o.forEach(traverse);
        } else {
          const record = o as Record<string, unknown>;
          if (record[key] === value) count++;
          Object.values(record).forEach(traverse);
        }
      }
    }
    traverse(obj);
    return count;
  }

  // Helper: Count nodes with a specific property
  function countProperty(obj: unknown, prop: string): number {
    let count = 0;
    function traverse(o: unknown): void {
      if (o && typeof o === "object") {
        if (Array.isArray(o)) {
          o.forEach(traverse);
        } else {
          const record = o as Record<string, unknown>;
          if (prop in record) count++;
          Object.values(record).forEach(traverse);
        }
      }
    }
    traverse(obj);
    return count;
  }

  TEST_FILES.forEach(({ name, desc }) => {
    describe(`${name} (${desc})`, () => {
      let optimizedData: SimplifiedDesign;

      beforeAll(() => {
        const filePath = path.join(expectedDir, `${name}-optimized.json`);
        optimizedData = JSON.parse(fs.readFileSync(filePath, "utf-8"));
      });

      it("should be idempotent (re-optimizing produces same result)", () => {
        // Optimize again
        const reOptimized = LayoutOptimizer.optimizeDesign(optimizedData);

        // Compare key metrics
        const originalAbsolute = countOccurrences(optimizedData, "position", "absolute");
        const reOptimizedAbsolute = countOccurrences(reOptimized, "position", "absolute");

        const originalFlex = countOccurrences(optimizedData, "display", "flex");
        const reOptimizedFlex = countOccurrences(reOptimized, "display", "flex");

        const originalGrid = countOccurrences(optimizedData, "display", "grid");
        const reOptimizedGrid = countOccurrences(reOptimized, "display", "grid");

        // Re-optimization should not change layout counts significantly
        // (small differences possible due to background merging on first pass)
        expect(reOptimizedAbsolute).toBeLessThanOrEqual(originalAbsolute);
        expect(reOptimizedFlex).toBeGreaterThanOrEqual(originalFlex);
        expect(reOptimizedGrid).toBeGreaterThanOrEqual(originalGrid);
      });

      it("should maintain or reduce absolute positioning count", () => {
        const reOptimized = LayoutOptimizer.optimizeDesign(optimizedData);

        const originalAbsolute = countOccurrences(optimizedData, "position", "absolute");
        const reOptimizedAbsolute = countOccurrences(reOptimized, "position", "absolute");

        // Absolute count should not increase
        expect(reOptimizedAbsolute).toBeLessThanOrEqual(originalAbsolute);
      });

      it("should maintain or reduce left/top property count", () => {
        const reOptimized = LayoutOptimizer.optimizeDesign(optimizedData);

        const originalLeft = countProperty(optimizedData, "left");
        const reOptimizedLeft = countProperty(reOptimized, "left");

        const originalTop = countProperty(optimizedData, "top");
        const reOptimizedTop = countProperty(reOptimized, "top");

        expect(reOptimizedLeft).toBeLessThanOrEqual(originalLeft);
        expect(reOptimizedTop).toBeLessThanOrEqual(originalTop);
      });

      it("should produce stable output on second re-optimization", () => {
        // First re-optimization
        const firstPass = LayoutOptimizer.optimizeDesign(optimizedData);
        // Second re-optimization
        const secondPass = LayoutOptimizer.optimizeDesign(firstPass);

        // After two passes, results should be identical (stable)
        const firstPassJson = JSON.stringify(firstPass);
        const secondPassJson = JSON.stringify(secondPass);

        expect(secondPassJson).toBe(firstPassJson);
      });

      it("should have expected optimization metrics", () => {
        const reOptimized = LayoutOptimizer.optimizeDesign(optimizedData);

        expect({
          absoluteCount: countOccurrences(reOptimized, "position", "absolute"),
          flexCount: countOccurrences(reOptimized, "display", "flex"),
          gridCount: countOccurrences(reOptimized, "display", "grid"),
          paddingCount: countProperty(reOptimized, "padding"),
        }).toMatchSnapshot();
      });
    });
  });
});

```

--------------------------------------------------------------------------------
/tests/unit/resources/figma-resources.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Figma Resources Unit Tests
 *
 * Tests the resource handlers that extract lightweight data from Figma files.
 */

import { describe, it, expect, vi } from "vitest";
import {
  getFileMetadata,
  getStyleTokens,
  getComponentList,
  getAssetList,
  createFileMetadataTemplate,
  createStylesTemplate,
  createComponentsTemplate,
  createAssetsTemplate,
  FIGMA_MCP_HELP,
} from "~/resources/figma-resources.js";
import type { SimplifiedDesign, SimplifiedNode } from "~/types/index.js";
import type { FigmaService } from "~/services/figma.js";

// ==================== Mock Types ====================

type MockFigmaService = Pick<
  FigmaService,
  "getFile" | "getNode" | "getImages" | "getImageFills" | "getRateLimitInfo"
>;

// ==================== Mock Data ====================

const createMockNode = (overrides: Partial<SimplifiedNode> = {}): SimplifiedNode => ({
  id: "node-1",
  name: "Test Node",
  type: "FRAME",
  ...overrides,
});

const createMockDesign = (overrides: Partial<SimplifiedDesign> = {}): SimplifiedDesign => ({
  name: "Test Design",
  lastModified: "2024-01-15T10:30:00Z",
  thumbnailUrl: "https://figma.com/thumbnail.png",
  nodes: [],
  ...overrides,
});

// Mock FigmaService
const createMockFigmaService = (design: SimplifiedDesign): MockFigmaService => ({
  getFile: vi.fn().mockResolvedValue(design),
  getNode: vi.fn().mockResolvedValue(design),
  getImages: vi.fn().mockResolvedValue([]),
  getImageFills: vi.fn().mockResolvedValue([]),
  getRateLimitInfo: vi.fn().mockReturnValue(null),
});

// ==================== Tests ====================

describe("Figma Resources", () => {
  describe("getFileMetadata", () => {
    it("should extract basic file metadata", async () => {
      const mockDesign = createMockDesign({
        name: "My Design File",
        lastModified: "2024-03-20T15:00:00Z",
        nodes: [
          createMockNode({
            id: "page-1",
            name: "Page 1",
            type: "CANVAS",
            children: [createMockNode(), createMockNode()],
          }),
          createMockNode({
            id: "page-2",
            name: "Page 2",
            type: "CANVAS",
            children: [createMockNode()],
          }),
        ],
      });

      const mockService = createMockFigmaService(mockDesign);
      const metadata = await getFileMetadata(mockService as FigmaService, "test-file-key");

      expect(metadata.name).toBe("My Design File");
      expect(metadata.lastModified).toBe("2024-03-20T15:00:00Z");
      expect(metadata.pages).toHaveLength(2);
      expect(metadata.pages[0]).toEqual({
        id: "page-1",
        name: "Page 1",
        childCount: 2,
      });
      expect(metadata.pages[1]).toEqual({
        id: "page-2",
        name: "Page 2",
        childCount: 1,
      });
    });

    it("should call getFile with depth 1", async () => {
      const mockDesign = createMockDesign();
      const mockService = createMockFigmaService(mockDesign);

      await getFileMetadata(mockService as FigmaService, "test-key");

      expect(mockService.getFile).toHaveBeenCalledWith("test-key", 1);
    });

    it("should handle files with no pages", async () => {
      const mockDesign = createMockDesign({ nodes: [] });
      const mockService = createMockFigmaService(mockDesign);

      const metadata = await getFileMetadata(mockService as FigmaService, "test-key");

      expect(metadata.pages).toHaveLength(0);
    });

    it("should filter out non-CANVAS nodes from pages", async () => {
      const mockDesign = createMockDesign({
        nodes: [
          createMockNode({ id: "page-1", name: "Page", type: "CANVAS" }),
          createMockNode({ id: "frame-1", name: "Frame", type: "FRAME" }),
          createMockNode({ id: "page-2", name: "Page 2", type: "CANVAS" }),
        ],
      });
      const mockService = createMockFigmaService(mockDesign);

      const metadata = await getFileMetadata(mockService as FigmaService, "test-key");

      expect(metadata.pages).toHaveLength(2);
      expect(metadata.pages.every((p) => p.name.startsWith("Page"))).toBe(true);
    });
  });

  describe("getStyleTokens", () => {
    it("should extract colors from node CSS", async () => {
      const mockDesign = createMockDesign({
        nodes: [
          createMockNode({
            name: "Primary Button",
            cssStyles: {
              backgroundColor: "#24C790",
              color: "#FFFFFF",
            },
          }),
        ],
      });
      const mockService = createMockFigmaService(mockDesign);

      const styles = await getStyleTokens(mockService as FigmaService, "test-key");

      expect(styles.colors.length).toBeGreaterThan(0);
      expect(styles.colors.some((c) => c.hex === "#24C790")).toBe(true);
    });

    it("should extract typography from node CSS", async () => {
      const mockDesign = createMockDesign({
        nodes: [
          createMockNode({
            name: "Heading",
            cssStyles: {
              fontFamily: "Inter",
              fontSize: "24px",
              fontWeight: "700",
              lineHeight: "32px",
            },
          }),
        ],
      });
      const mockService = createMockFigmaService(mockDesign);

      const styles = await getStyleTokens(mockService as FigmaService, "test-key");

      expect(styles.typography.length).toBeGreaterThan(0);
      expect(styles.typography[0]).toMatchObject({
        fontFamily: "Inter",
        fontSize: 24,
        fontWeight: 700,
      });
    });

    it("should extract shadow effects", async () => {
      const mockDesign = createMockDesign({
        nodes: [
          createMockNode({
            name: "Card",
            cssStyles: {
              boxShadow: "0px 4px 8px rgba(0, 0, 0, 0.1)",
            },
          }),
        ],
      });
      const mockService = createMockFigmaService(mockDesign);

      const styles = await getStyleTokens(mockService as FigmaService, "test-key");

      expect(styles.effects.length).toBeGreaterThan(0);
      expect(styles.effects[0].type).toBe("shadow");
    });

    it("should deduplicate colors", async () => {
      const mockDesign = createMockDesign({
        nodes: [
          createMockNode({ cssStyles: { backgroundColor: "#FF0000" } }),
          createMockNode({ cssStyles: { backgroundColor: "#FF0000" } }),
          createMockNode({ cssStyles: { backgroundColor: "#00FF00" } }),
        ],
      });
      const mockService = createMockFigmaService(mockDesign);

      const styles = await getStyleTokens(mockService as FigmaService, "test-key");

      const redColors = styles.colors.filter((c) => c.hex === "#FF0000");
      expect(redColors.length).toBe(1);
    });

    it("should limit results to avoid token bloat", async () => {
      // Create many nodes with unique colors
      const nodes = Array.from({ length: 50 }, (_, i) =>
        createMockNode({
          name: `Node ${i}`,
          cssStyles: { backgroundColor: `#${i.toString(16).padStart(6, "0")}` },
        }),
      );
      const mockDesign = createMockDesign({ nodes });
      const mockService = createMockFigmaService(mockDesign);

      const styles = await getStyleTokens(mockService as FigmaService, "test-key");

      expect(styles.colors.length).toBeLessThanOrEqual(20);
    });

    it("should recursively extract from children", async () => {
      const mockDesign = createMockDesign({
        nodes: [
          createMockNode({
            name: "Parent",
            children: [
              createMockNode({
                name: "Child",
                cssStyles: { backgroundColor: "#AABBCC" },
              }),
            ],
          }),
        ],
      });
      const mockService = createMockFigmaService(mockDesign);

      const styles = await getStyleTokens(mockService as FigmaService, "test-key");

      expect(styles.colors.some((c) => c.hex === "#AABBCC")).toBe(true);
    });
  });

  describe("getComponentList", () => {
    it("should find COMPONENT nodes", async () => {
      const mockDesign = createMockDesign({
        nodes: [
          createMockNode({
            id: "comp-1",
            name: "Button",
            type: "COMPONENT",
          }),
        ],
      });
      const mockService = createMockFigmaService(mockDesign);

      const components = await getComponentList(mockService as FigmaService, "test-key");

      expect(components).toHaveLength(1);
      expect(components[0]).toMatchObject({
        id: "comp-1",
        name: "Button",
        type: "COMPONENT",
      });
    });

    it("should find COMPONENT_SET nodes with variants", async () => {
      const mockDesign = createMockDesign({
        nodes: [
          createMockNode({
            id: "set-1",
            name: "Button",
            type: "COMPONENT_SET",
            children: [
              createMockNode({ name: "Primary" }),
              createMockNode({ name: "Secondary" }),
              createMockNode({ name: "Outline" }),
            ],
          }),
        ],
      });
      const mockService = createMockFigmaService(mockDesign);

      const components = await getComponentList(mockService as FigmaService, "test-key");

      expect(components).toHaveLength(1);
      expect(components[0].type).toBe("COMPONENT_SET");
      expect(components[0].variants).toEqual(["Primary", "Secondary", "Outline"]);
    });

    it("should limit variants to 5", async () => {
      const variants = Array.from({ length: 10 }, (_, i) =>
        createMockNode({ name: `Variant ${i}` }),
      );
      const mockDesign = createMockDesign({
        nodes: [
          createMockNode({
            id: "set-1",
            name: "Button",
            type: "COMPONENT_SET",
            children: variants,
          }),
        ],
      });
      const mockService = createMockFigmaService(mockDesign);

      const components = await getComponentList(mockService as FigmaService, "test-key");

      expect(components[0].variants).toHaveLength(5);
    });

    it("should find nested components", async () => {
      const mockDesign = createMockDesign({
        nodes: [
          createMockNode({
            name: "Page",
            type: "CANVAS",
            children: [
              createMockNode({
                name: "Components",
                type: "FRAME",
                children: [
                  createMockNode({ id: "c1", name: "Button", type: "COMPONENT" }),
                  createMockNode({ id: "c2", name: "Input", type: "COMPONENT" }),
                ],
              }),
            ],
          }),
        ],
      });
      const mockService = createMockFigmaService(mockDesign);

      const components = await getComponentList(mockService as FigmaService, "test-key");

      expect(components).toHaveLength(2);
    });

    it("should limit to 50 components", async () => {
      const nodes = Array.from({ length: 60 }, (_, i) =>
        createMockNode({
          id: `comp-${i}`,
          name: `Component ${i}`,
          type: "COMPONENT",
        }),
      );
      const mockDesign = createMockDesign({ nodes });
      const mockService = createMockFigmaService(mockDesign);

      const components = await getComponentList(mockService as FigmaService, "test-key");

      expect(components.length).toBeLessThanOrEqual(50);
    });
  });

  describe("getAssetList", () => {
    it("should find nodes with exportInfo", async () => {
      const mockDesign = createMockDesign({
        nodes: [
          createMockNode({
            id: "icon-1",
            name: "arrow-right",
            type: "VECTOR",
            exportInfo: { type: "IMAGE", format: "SVG" },
          }),
        ],
      });
      const mockService = createMockFigmaService(mockDesign);

      const assets = await getAssetList(mockService as FigmaService, "test-key");

      expect(assets).toHaveLength(1);
      expect(assets[0]).toMatchObject({
        nodeId: "icon-1",
        name: "arrow-right",
        type: "icon",
        exportFormats: ["SVG"],
      });
    });

    it("should identify icons by type VECTOR", async () => {
      const mockDesign = createMockDesign({
        nodes: [
          createMockNode({
            id: "icon-1",
            name: "small-icon",
            type: "VECTOR",
            exportInfo: { type: "IMAGE", format: "SVG" },
          }),
        ],
      });
      const mockService = createMockFigmaService(mockDesign);

      const assets = await getAssetList(mockService as FigmaService, "test-key");

      expect(assets[0].type).toBe("icon");
    });

    it("should identify large exports as vector type", async () => {
      const mockDesign = createMockDesign({
        nodes: [
          createMockNode({
            id: "illustration-1",
            name: "hero-image",
            type: "FRAME",
            exportInfo: { type: "IMAGE_GROUP", format: "SVG" },
          }),
        ],
      });
      const mockService = createMockFigmaService(mockDesign);

      const assets = await getAssetList(mockService as FigmaService, "test-key");

      expect(assets[0].type).toBe("vector");
    });

    it("should find nodes with image fills", async () => {
      const mockDesign = createMockDesign({
        nodes: [
          createMockNode({
            id: "img-1",
            name: "photo",
            type: "RECTANGLE",
            fills: [{ type: "IMAGE", imageRef: "img:abc123" }],
          }),
        ],
      });
      const mockService = createMockFigmaService(mockDesign);

      const assets = await getAssetList(mockService as FigmaService, "test-key");

      expect(assets).toHaveLength(1);
      expect(assets[0]).toMatchObject({
        nodeId: "img-1",
        name: "photo",
        type: "image",
        imageRef: "img:abc123",
      });
    });

    it("should find nested assets", async () => {
      const mockDesign = createMockDesign({
        nodes: [
          createMockNode({
            name: "Card",
            children: [
              createMockNode({
                id: "icon",
                name: "icon",
                type: "VECTOR",
                exportInfo: { type: "IMAGE", format: "SVG" },
              }),
              createMockNode({
                id: "image",
                name: "thumbnail",
                fills: [{ type: "IMAGE", imageRef: "img:xyz" }],
              }),
            ],
          }),
        ],
      });
      const mockService = createMockFigmaService(mockDesign);

      const assets = await getAssetList(mockService as FigmaService, "test-key");

      expect(assets).toHaveLength(2);
    });

    it("should limit to 100 assets", async () => {
      const nodes = Array.from({ length: 150 }, (_, i) =>
        createMockNode({
          id: `asset-${i}`,
          name: `Asset ${i}`,
          type: "VECTOR",
          exportInfo: { type: "IMAGE", format: "SVG" },
        }),
      );
      const mockDesign = createMockDesign({ nodes });
      const mockService = createMockFigmaService(mockDesign);

      const assets = await getAssetList(mockService as FigmaService, "test-key");

      expect(assets.length).toBeLessThanOrEqual(100);
    });
  });

  describe("Resource Templates", () => {
    it("should create file metadata template with correct URI pattern", () => {
      const template = createFileMetadataTemplate();
      expect(template.uriTemplate.toString()).toBe("figma://file/{fileKey}");
    });

    it("should create styles template with correct URI pattern", () => {
      const template = createStylesTemplate();
      expect(template.uriTemplate.toString()).toBe("figma://file/{fileKey}/styles");
    });

    it("should create components template with correct URI pattern", () => {
      const template = createComponentsTemplate();
      expect(template.uriTemplate.toString()).toBe("figma://file/{fileKey}/components");
    });

    it("should create assets template with correct URI pattern", () => {
      const template = createAssetsTemplate();
      expect(template.uriTemplate.toString()).toBe("figma://file/{fileKey}/assets");
    });
  });

  describe("Help Content", () => {
    it("should contain resource documentation", () => {
      expect(FIGMA_MCP_HELP).toContain("figma://file/{fileKey}");
      expect(FIGMA_MCP_HELP).toContain("figma://file/{fileKey}/styles");
      expect(FIGMA_MCP_HELP).toContain("figma://file/{fileKey}/components");
      expect(FIGMA_MCP_HELP).toContain("figma://file/{fileKey}/assets");
    });

    it("should explain token costs", () => {
      expect(FIGMA_MCP_HELP).toContain("Token cost");
    });

    it("should explain how to get fileKey", () => {
      expect(FIGMA_MCP_HELP).toContain("fileKey");
      expect(FIGMA_MCP_HELP).toContain("figma.com");
    });
  });
});

```

--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------

```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { FigmaService, type FigmaError } from "./services/figma.js";
import express, { type Request, type Response } from "express";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { type IncomingMessage, type ServerResponse } from "http";
import { type Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import type { SimplifiedDesign } from "./types/index.js";
import {
  DESIGN_TO_CODE_PROMPT,
  COMPONENT_ANALYSIS_PROMPT,
  STYLE_EXTRACTION_PROMPT,
} from "./prompts/index.js";
import {
  getFileMetadata,
  getStyleTokens,
  getComponentList,
  getAssetList,
  createFileMetadataTemplate,
  createStylesTemplate,
  createComponentsTemplate,
  createAssetsTemplate,
  FIGMA_MCP_HELP,
} from "./resources/index.js";

// ==================== Logging Utilities ====================

export const Logger = {
  log: (..._args: unknown[]) => {},
  error: (..._args: unknown[]) => {},
};

// ==================== Error Formatting ====================

/**
 * Check if error is a Figma API error
 */
function isFigmaError(error: unknown): error is FigmaError {
  return (
    typeof error === "object" &&
    error !== null &&
    "status" in error &&
    typeof (error as FigmaError).status === "number"
  );
}

/**
 * Format error information for AI understanding
 */
function formatErrorForAI(error: unknown, context: string): string {
  if (isFigmaError(error)) {
    const parts: string[] = [`[Figma API Error] ${context}`];
    parts.push(`Status: ${error.status}`);
    parts.push(`Message: ${error.err}`);

    if (error.rateLimitInfo) {
      const { remaining, resetAfter, retryAfter } = error.rateLimitInfo;
      if (remaining !== null) parts.push(`Rate Limit Remaining: ${remaining}`);
      if (retryAfter !== null) parts.push(`Retry After: ${retryAfter} seconds`);
      if (resetAfter !== null) parts.push(`Reset After: ${resetAfter} seconds`);
    }

    return parts.join("\n");
  }

  if (error instanceof Error) {
    return `[Error] ${context}: ${error.message}`;
  }

  return `[Error] ${context}: ${String(error)}`;
}

// ==================== MCP Server ====================

export class FigmaMcpServer {
  private readonly server: McpServer;
  private readonly figmaService: FigmaService;
  private sseTransport: SSEServerTransport | null = null;

  constructor(figmaApiKey: string) {
    this.figmaService = new FigmaService(figmaApiKey);
    this.server = new McpServer(
      {
        name: "Figma MCP Server",
        version: "1.0.2",
      },
      {
        capabilities: {
          logging: {},
          tools: {},
          prompts: {},
          resources: {},
        },
      },
    );

    this.registerTools();
    this.registerPrompts();
    this.registerResources();
  }

  private registerTools(): void {
    // Tool: Get Figma data
    this.server.tool(
      "get_figma_data",
      "Get layout and style information from a Figma file or specific node. " +
        "Returns simplified design data including CSS styles, text content, and export info. " +
        "Results are cached for 24 hours to reduce API calls.",
      {
        fileKey: z
          .string()
          .describe(
            "The key of the Figma file to fetch, found in URL like figma.com/(file|design)/<fileKey>/...",
          ),
        nodeId: z
          .string()
          .optional()
          .describe(
            "The ID of a specific node to fetch (e.g., '1234:5678'), found as URL parameter node-id=<nodeId>. Use this for better performance with large files.",
          ),
        depth: z
          .number()
          .optional()
          .describe(
            "How many levels deep to traverse the node tree (1-100). Only use if explicitly needed.",
          ),
      },
      async ({ fileKey, nodeId, depth }) => {
        try {
          Logger.log(
            `Fetching ${depth ? `${depth} layers deep` : "all layers"} of ${
              nodeId ? `node ${nodeId} from file` : `full file`
            } ${fileKey}`,
          );

          let file: SimplifiedDesign;
          if (nodeId) {
            file = await this.figmaService.getNode(fileKey, nodeId, depth);
          } else {
            file = await this.figmaService.getFile(fileKey, depth);
          }

          Logger.log(`Successfully fetched file: ${file.name}`);
          const { nodes, ...metadata } = file;

          // Serialize in segments to handle large files
          const nodesJson = `[${nodes.map((node) => JSON.stringify(node, null, 2)).join(",")}]`;
          const metadataJson = JSON.stringify(metadata, null, 2);
          const resultJson = `{ "metadata": ${metadataJson}, "nodes": ${nodesJson} }`;

          // Add cache status information
          const rateLimitInfo = this.figmaService.getRateLimitInfo();
          let statusNote = "";
          if (rateLimitInfo && rateLimitInfo.remaining !== null) {
            statusNote = `\n\n[API Status] Rate limit remaining: ${rateLimitInfo.remaining}`;
          }

          return {
            content: [{ type: "text", text: resultJson + statusNote }],
          };
        } catch (error) {
          Logger.error(`Error fetching file ${fileKey}:`, error);
          const errorMessage = formatErrorForAI(
            error,
            `Failed to fetch Figma data for file ${fileKey}`,
          );
          return {
            isError: true,
            content: [{ type: "text", text: errorMessage }],
          };
        }
      },
    );

    // Tool: Download images
    this.server.tool(
      "download_figma_images",
      "Download SVG and PNG images from a Figma file. " +
        "Supports both rendered node images and image fills. " +
        "Images are cached locally to avoid repeated downloads.",
      {
        fileKey: z.string().describe("The key of the Figma file containing the images"),
        nodes: z
          .object({
            nodeId: z
              .string()
              .describe("The ID of the Figma image node to fetch (e.g., '1234:5678')"),
            imageRef: z
              .string()
              .optional()
              .describe(
                "Required for image fills (background images). Leave blank for vector/icon SVGs.",
              ),
            fileName: z
              .string()
              .describe("The local filename to save as (e.g., 'icon.svg', 'photo.png')"),
          })
          .array()
          .describe("Array of image nodes to download"),
        localPath: z
          .string()
          .describe(
            "Absolute path to the directory where images should be saved. Directories will be created if needed.",
          ),
      },
      async ({ fileKey, nodes, localPath }) => {
        try {
          // Classify processing: image fills vs rendered nodes
          const imageFills = nodes.filter(({ imageRef }) => !!imageRef) as {
            nodeId: string;
            imageRef: string;
            fileName: string;
          }[];

          const renderRequests = nodes
            .filter(({ imageRef }) => !imageRef)
            .map(({ nodeId, fileName }) => ({
              nodeId,
              fileName,
              fileType: fileName.toLowerCase().endsWith(".svg")
                ? ("svg" as const)
                : ("png" as const),
            }));

          // Execute sequentially to reduce rate limit risk
          const fillResults = await this.figmaService.getImageFills(fileKey, imageFills, localPath);
          const renderResults = await this.figmaService.getImages(
            fileKey,
            renderRequests,
            localPath,
          );

          const allDownloads = [...fillResults, ...renderResults];
          const successfulDownloads = allDownloads.filter((path) => path && path.length > 0);
          const failedCount = allDownloads.length - successfulDownloads.length;

          let resultMessage: string;
          if (successfulDownloads.length === allDownloads.length) {
            resultMessage = `Successfully downloaded ${successfulDownloads.length} images:\n${successfulDownloads.join("\n")}`;
          } else if (successfulDownloads.length > 0) {
            resultMessage = `Downloaded ${successfulDownloads.length}/${allDownloads.length} images (${failedCount} failed):\n${successfulDownloads.join("\n")}`;
          } else {
            resultMessage = `Failed to download any images. Please check the node IDs and try again.`;
          }

          return {
            content: [{ type: "text", text: resultMessage }],
          };
        } catch (error) {
          Logger.error(`Error downloading images from file ${fileKey}:`, error);
          const errorMessage = formatErrorForAI(
            error,
            `Failed to download images from file ${fileKey}`,
          );
          return {
            isError: true,
            content: [{ type: "text", text: errorMessage }],
          };
        }
      },
    );
  }

  private registerPrompts(): void {
    // Prompt: Design to Code - Full workflow
    this.server.prompt(
      "design_to_code",
      "Complete workflow for converting Figma designs to production-ready code with project analysis",
      {
        framework: z
          .enum(["react", "vue", "html", "auto"])
          .optional()
          .describe("Target framework for code generation (default: auto-detect from project)"),
        includeResponsive: z
          .boolean()
          .optional()
          .describe("Include responsive/mobile adaptation guidelines (default: true)"),
      },
      async ({ framework, includeResponsive }) => {
        let prompt = DESIGN_TO_CODE_PROMPT;

        // Add framework-specific context
        if (framework && framework !== "auto") {
          prompt += `\n\n## Framework Context\nTarget framework: **${framework.toUpperCase()}**\n`;
          if (framework === "vue") {
            prompt += `- USE Vue 3 Composition API with <script setup>
- USE defineProps/defineEmits for component interface
- PREFER template syntax over JSX`;
          } else if (framework === "react") {
            prompt += `- USE functional components with hooks
- USE TypeScript for props interface
- PREFER named exports for components`;
          }
        }

        // Add responsive guidelines toggle
        if (includeResponsive === false) {
          prompt += `\n\n## Note\nSkip Phase 6 (Responsive Adaptation) - desktop only implementation required.`;
        }

        return {
          messages: [
            {
              role: "user",
              content: {
                type: "text",
                text: prompt,
              },
            },
          ],
        };
      },
    );

    // Prompt: Component Analysis
    this.server.prompt(
      "analyze_components",
      "Analyze Figma design to identify optimal component structure and reusability",
      {},
      async () => {
        return {
          messages: [
            {
              role: "user",
              content: {
                type: "text",
                text: COMPONENT_ANALYSIS_PROMPT,
              },
            },
          ],
        };
      },
    );

    // Prompt: Style Extraction
    this.server.prompt(
      "extract_styles",
      "Extract design tokens (colors, typography, spacing) from Figma design data",
      {},
      async () => {
        return {
          messages: [
            {
              role: "user",
              content: {
                type: "text",
                text: STYLE_EXTRACTION_PROMPT,
              },
            },
          ],
        };
      },
    );
  }

  private registerResources(): void {
    // Static Resource: Help guide
    this.server.resource(
      "figma_help",
      "figma://help",
      {
        description: "Figma MCP Server usage guide and resource documentation",
        mimeType: "text/markdown",
      },
      async () => {
        return {
          contents: [
            {
              uri: "figma://help",
              mimeType: "text/markdown",
              text: FIGMA_MCP_HELP,
            },
          ],
        };
      },
    );

    // Template Resource: File metadata
    this.server.resource(
      "figma_file",
      createFileMetadataTemplate(),
      {
        description: "Get Figma file metadata (name, pages, last modified). Low token cost (~200).",
        mimeType: "application/json",
      },
      async (uri, variables) => {
        const fileKey = variables.fileKey as string;
        if (!fileKey) {
          throw new Error("fileKey is required");
        }

        try {
          const metadata = await getFileMetadata(this.figmaService, fileKey);
          return {
            contents: [
              {
                uri: uri.href,
                mimeType: "application/json",
                text: JSON.stringify(metadata, null, 2),
              },
            ],
          };
        } catch (error) {
          const message = error instanceof Error ? error.message : String(error);
          throw new Error(`Failed to fetch file metadata: ${message}`);
        }
      },
    );

    // Template Resource: Style tokens
    this.server.resource(
      "figma_styles",
      createStylesTemplate(),
      {
        description:
          "Extract design tokens (colors, typography, effects) from Figma file. Token cost ~500.",
        mimeType: "application/json",
      },
      async (uri, variables) => {
        const fileKey = variables.fileKey as string;
        if (!fileKey) {
          throw new Error("fileKey is required");
        }

        try {
          const styles = await getStyleTokens(this.figmaService, fileKey);
          return {
            contents: [
              {
                uri: uri.href,
                mimeType: "application/json",
                text: JSON.stringify(styles, null, 2),
              },
            ],
          };
        } catch (error) {
          const message = error instanceof Error ? error.message : String(error);
          throw new Error(`Failed to fetch styles: ${message}`);
        }
      },
    );

    // Template Resource: Component list
    this.server.resource(
      "figma_components",
      createComponentsTemplate(),
      {
        description: "List all components and component sets in Figma file. Token cost ~300.",
        mimeType: "application/json",
      },
      async (uri, variables) => {
        const fileKey = variables.fileKey as string;
        if (!fileKey) {
          throw new Error("fileKey is required");
        }

        try {
          const components = await getComponentList(this.figmaService, fileKey);
          return {
            contents: [
              {
                uri: uri.href,
                mimeType: "application/json",
                text: JSON.stringify(components, null, 2),
              },
            ],
          };
        } catch (error) {
          const message = error instanceof Error ? error.message : String(error);
          throw new Error(`Failed to fetch components: ${message}`);
        }
      },
    );

    // Template Resource: Asset list
    this.server.resource(
      "figma_assets",
      createAssetsTemplate(),
      {
        description:
          "List exportable assets (icons, images, vectors) with node IDs for download. Token cost ~400.",
        mimeType: "application/json",
      },
      async (uri, variables) => {
        const fileKey = variables.fileKey as string;
        if (!fileKey) {
          throw new Error("fileKey is required");
        }

        try {
          const assets = await getAssetList(this.figmaService, fileKey);
          return {
            contents: [
              {
                uri: uri.href,
                mimeType: "application/json",
                text: JSON.stringify(assets, null, 2),
              },
            ],
          };
        } catch (error) {
          const message = error instanceof Error ? error.message : String(error);
          throw new Error(`Failed to fetch assets: ${message}`);
        }
      },
    );
  }

  async connect(transport: Transport): Promise<void> {
    await this.server.connect(transport);

    Logger.log = (...args: unknown[]) => {
      this.server.server.sendLoggingMessage({
        level: "info",
        data: args,
      });
    };
    Logger.error = (...args: unknown[]) => {
      this.server.server.sendLoggingMessage({
        level: "error",
        data: args,
      });
    };

    Logger.log("Server connected and ready to process requests");
  }

  async startHttpServer(port: number): Promise<void> {
    const app = express();

    app.get("/sse", async (req: Request, res: Response) => {
      console.log("New SSE connection established");
      this.sseTransport = new SSEServerTransport(
        "/messages",
        res as unknown as ServerResponse<IncomingMessage>,
      );
      await this.server.connect(this.sseTransport);
    });

    app.post("/messages", async (req: Request, res: Response) => {
      if (!this.sseTransport) {
        res.sendStatus(400);
        return;
      }
      await this.sseTransport.handlePostMessage(
        req as unknown as IncomingMessage,
        res as unknown as ServerResponse<IncomingMessage>,
      );
    });

    Logger.log = console.log;
    Logger.error = console.error;

    app.listen(port, () => {
      Logger.log(`HTTP server listening on port ${port}`);
      Logger.log(`SSE endpoint available at http://localhost:${port}/sse`);
      Logger.log(`Message endpoint available at http://localhost:${port}/messages`);
    });
  }
}

```

--------------------------------------------------------------------------------
/tests/utils/viewer.html:
--------------------------------------------------------------------------------

```html
<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Figma 节点 CSS 样式查看器</title>
    <style>
        :root {
            --primary-color: #1E88E5;
            --secondary-color: #757575;
            --background-color: #FAFAFA;
            --card-background: #FFFFFF;
            --border-color: #E0E0E0;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
            margin: 0;
            padding: 20px;
            background-color: var(--background-color);
            color: #333;
        }

        .container {
            max-width: 1200px;
            margin: 0 auto;
            background-color: var(--card-background);
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
            padding: 20px;
        }

        h1 {
            color: var(--primary-color);
            border-bottom: 1px solid var(--border-color);
            padding-bottom: 10px;
            margin-top: 0;
        }

        .info-box {
            background-color: #E3F2FD;
            border-left: 4px solid var(--primary-color);
            padding: 10px 15px;
            margin-bottom: 20px;
            border-radius: 4px;
        }

        .file-input-container {
            display: flex;
            margin-bottom: 20px;
            align-items: center;
            flex-wrap: wrap;
            gap: 10px;
        }

        input[type="file"] {
            flex: 1;
            min-width: 300px;
            padding: 8px;
            border: 1px solid var(--border-color);
            border-radius: 4px;
        }

        button {
            background-color: var(--primary-color);
            color: white;
            border: none;
            padding: 8px 16px;
            border-radius: 4px;
            cursor: pointer;
            transition: background-color 0.2s;
        }

        button:hover {
            background-color: #1565C0;
        }

        .tabs {
            display: flex;
            margin-bottom: 20px;
            border-bottom: 1px solid var(--border-color);
        }

        .tab {
            padding: 10px 20px;
            cursor: pointer;
            border-bottom: 2px solid transparent;
        }

        .tab.active {
            color: var(--primary-color);
            border-bottom: 2px solid var(--primary-color);
            font-weight: 500;
        }

        .tab-content {
            display: none;
        }

        .tab-content.active {
            display: block;
        }

        .nodes {
            font-family: monospace;
            white-space: pre-wrap;
            padding: 15px;
            background-color: #F5F5F5;
            border-radius: 4px;
            overflow: auto;
            max-height: 600px;
        }

        .css-styles {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
            gap: 20px;
        }

        .style-card {
            border: 1px solid var(--border-color);
            border-radius: 6px;
            overflow: hidden;
        }

        .style-preview {
            height: 120px;
            display: flex;
            justify-content: center;
            align-items: center;
        }

        .style-info {
            padding: 15px;
            border-top: 1px solid var(--border-color);
            background-color: #F5F5F5;
        }

        .style-name {
            font-weight: 500;
            margin-bottom: 5px;
        }

        .style-properties {
            font-family: monospace;
            font-size: 13px;
        }

        .property {
            margin: 3px 0;
        }

        .color-box {
            display: inline-block;
            width: 16px;
            height: 16px;
            border-radius: 3px;
            margin-right: 6px;
            vertical-align: middle;
            border: 1px solid rgba(0, 0, 0, 0.1);
        }

        .search-container {
            margin-bottom: 15px;
        }

        #nodeSearch {
            width: 100%;
            padding: 8px;
            border: 1px solid var(--border-color);
            border-radius: 4px;
            margin-bottom: 10px;
        }

        .tree-view {
            font-family: monospace;
            line-height: 1.5;
        }

        .tree-item {
            margin: 2px 0;
            cursor: pointer;
        }

        .tree-toggle {
            display: inline-block;
            width: 16px;
            text-align: center;
            user-select: none;
        }

        .tree-content {
            padding-left: 20px;
            display: none;
        }

        .tree-content.expanded {
            display: block;
        }

        .selected {
            background-color: #E3F2FD;
            border-radius: 3px;
        }

        #nodeDetails {
            margin-top: 20px;
            padding: 15px;
            background-color: #F5F5F5;
            border-radius: 4px;
            display: none;
        }

        .detail-grid {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 15px;
        }

        @media (max-width: 768px) {
            .detail-grid {
                grid-template-columns: 1fr;
            }
        }

        .detail-section {
            border: 1px solid var(--border-color);
            border-radius: 4px;
            padding: 10px;
            background-color: white;
        }

        .detail-title {
            font-weight: 500;
            margin-bottom: 10px;
            color: var(--primary-color);
        }

        .detail-content {
            max-height: 300px;
            overflow: auto;
        }

        .css-preview {
            border: 1px solid #ddd;
            padding: 15px;
            margin-top: 10px;
            border-radius: 4px;
        }
    </style>
</head>

<body>
    <div class="container">
        <h1>Figma 节点 CSS 样式查看器</h1>

        <div class="info-box">
            此工具用于查看Figma节点数据及其CSS样式转换结果。您可以上传JSON文件或加载示例数据。
        </div>

        <div class="file-input-container">
            <input type="file" id="fileInput" accept=".json">
            <button id="loadFile">加载文件</button>
            <button id="loadSample">加载示例数据</button>
        </div>

        <div class="tabs">
            <div class="tab active" data-tab="nodeTree">节点树</div>
            <div class="tab" data-tab="cssStyles">CSS 样式</div>
        </div>

        <div id="nodeTree" class="tab-content active">
            <div class="search-container">
                <input type="text" id="nodeSearch" placeholder="搜索节点名称...">
            </div>
            <div class="tree-view" id="nodeTreeView"></div>
            <div id="nodeDetails">
                <h3>节点详情</h3>
                <div class="detail-grid">
                    <div class="detail-section">
                        <div class="detail-title">基本信息</div>
                        <div class="detail-content" id="nodeBasicInfo"></div>
                    </div>
                    <div class="detail-section">
                        <div class="detail-title">CSS 样式</div>
                        <div class="detail-content" id="nodeCssStyles"></div>
                        <div class="css-preview" id="cssPreview"></div>
                    </div>
                </div>
            </div>
        </div>

        <div id="cssStyles" class="tab-content">
            <div id="cssStylesContent" class="css-styles"></div>
        </div>
    </div>

    <script>
        let figmaData = null

        // DOM元素
        const fileInput = document.getElementById('fileInput')
        const loadFileBtn = document.getElementById('loadFile')
        const loadSampleBtn = document.getElementById('loadSample')
        const tabs = document.querySelectorAll('.tab')
        const tabContents = document.querySelectorAll('.tab-content')
        const nodeTreeView = document.getElementById('nodeTreeView')
        const nodeSearch = document.getElementById('nodeSearch')
        const nodeDetails = document.getElementById('nodeDetails')
        const nodeBasicInfo = document.getElementById('nodeBasicInfo')
        const nodeCssStyles = document.getElementById('nodeCssStyles')
        const cssPreview = document.getElementById('cssPreview')
        const cssStylesContent = document.getElementById('cssStylesContent')

        // 初始化
        document.addEventListener('DOMContentLoaded', () => {
            // 加载示例数据(如果在同目录下存在)
            try {
                fetch('./simplified-with-css.json')
                    .then(response => {
                        if (!response.ok) throw new Error('示例数据未找到')
                        return response.json()
                    })
                    .then(data => {
                        figmaData = data
                        renderData()
                    })
                    .catch(err => console.log('未找到示例数据,请上传文件'))
            } catch (e) {
                console.log('未找到示例数据,请上传文件')
            }
        })

        // 事件监听器
        loadFileBtn.addEventListener('click', () => {
            if (fileInput.files.length > 0) {
                const file = fileInput.files[0]
                const reader = new FileReader()

                reader.onload = (e) => {
                    try {
                        figmaData = JSON.parse(e.target.result)
                        renderData()
                    } catch (err) {
                        alert('JSON解析错误: ' + err.message)
                    }
                }

                reader.readAsText(file)
            } else {
                alert('请选择一个JSON文件')
            }
        })

        loadSampleBtn.addEventListener('click', async () => {
            try {
                const response = await fetch('./simplified-with-css.json')
                if (!response.ok) throw new Error('示例数据未找到')
                figmaData = await response.json()
                renderData()
            } catch (err) {
                alert('加载示例数据失败: ' + err.message)
            }
        })

        // 标签切换
        tabs.forEach(tab => {
            tab.addEventListener('click', () => {
                const tabId = tab.getAttribute('data-tab')

                tabs.forEach(t => t.classList.remove('active'))
                tabContents.forEach(tc => tc.classList.remove('active'))

                tab.classList.add('active')
                document.getElementById(tabId).classList.add('active')
            })
        })

        // 搜索功能
        nodeSearch.addEventListener('input', () => {
            const searchTerm = nodeSearch.value.toLowerCase()
            const treeItems = document.querySelectorAll('.tree-item')

            treeItems.forEach(item => {
                const text = item.textContent.toLowerCase()
                if (text.includes(searchTerm)) {
                    item.style.display = 'block'

                    // 展开父级
                    let parent = item.parentElement
                    while (parent && parent.classList.contains('tree-content')) {
                        parent.classList.add('expanded')
                        parent = parent.parentElement.parentElement
                    }
                } else {
                    item.style.display = 'none'
                }
            })
        })

        // 渲染数据
        function renderData() {
            if (!figmaData) return

            // 渲染节点树
            renderNodeTree()

            // 渲染CSS样式
            renderCssStyles()
        }

        // 渲染节点树
        function renderNodeTree() {
            nodeTreeView.innerHTML = ''

            if (figmaData.nodes && Array.isArray(figmaData.nodes)) {
                figmaData.nodes.forEach(node => {
                    nodeTreeView.appendChild(createTreeItem(node))
                })
            }
        }

        // 创建树项
        function createTreeItem(node, level = 0) {
            const item = document.createElement('div')
            item.className = 'tree-item'
            item.dataset.nodeId = node.id || ''

            const hasChildren = node.children && node.children.length > 0

            const toggle = document.createElement('span')
            toggle.className = 'tree-toggle'
            toggle.textContent = hasChildren ? '▶' : ' '

            const label = document.createElement('span')
            label.className = 'tree-label'
            label.textContent = `${node.name || 'Unnamed'} (${node.type || 'Unknown'})`

            item.appendChild(toggle)
            item.appendChild(label)

            if (hasChildren) {
                const content = document.createElement('div')
                content.className = 'tree-content'

                node.children.forEach(child => {
                    content.appendChild(createTreeItem(child, level + 1))
                })

                toggle.addEventListener('click', () => {
                    toggle.textContent = content.classList.toggle('expanded') ? '▼' : '▶'
                })

                item.appendChild(content)
            }

            // 点击查看节点详情
            item.addEventListener('click', (e) => {
                if (e.target !== toggle) {
                    document.querySelectorAll('.tree-item').forEach(i => i.classList.remove('selected'))
                    item.classList.add('selected')
                    showNodeDetails(node)
                }
                e.stopPropagation()
            })

            return item
        }

        // 显示节点详情
        function showNodeDetails(node) {
            nodeDetails.style.display = 'block'

            // 基本信息
            nodeBasicInfo.innerHTML = `
                <div><strong>ID:</strong> ${node.id || 'N/A'}</div>
                <div><strong>名称:</strong> ${node.name || 'Unnamed'}</div>
                <div><strong>类型:</strong> ${node.type || 'Unknown'}</div>
                ${node.boundingBox ? `
                <div><strong>位置:</strong> X: ${node.boundingBox.x.toFixed(2)}, Y: ${node.boundingBox.y.toFixed(2)}</div>
                <div><strong>尺寸:</strong> W: ${node.boundingBox.width.toFixed(2)}, H: ${node.boundingBox.height.toFixed(2)}</div>
                ` : ''}
            `

            // CSS样式
            if (node.cssStyles && Object.keys(node.cssStyles).length > 0) {
                let cssStylesHtml = '<div class="properties">'

                for (const [property, value] of Object.entries(node.cssStyles)) {
                    cssStylesHtml += `
                        <div class="property">
                            ${property.includes('color') || property.includes('background') ?
                            `<span class="color-box" style="background-color: ${value}"></span>` : ''}
                            <strong>${property}:</strong> ${value}
                        </div>
                    `
                }

                cssStylesHtml += '</div>'
                nodeCssStyles.innerHTML = cssStylesHtml

                // CSS预览
                let styles = ''
                for (const [property, value] of Object.entries(node.cssStyles)) {
                    styles += `${property}: ${value};\n`
                }

                cssPreview.innerHTML = `
                    <div class="detail-title">预览</div>
                    <div style="${styles} border: 1px dashed #ccc; min-height: 50px; display: flex; align-items: center; justify-content: center;">
                        ${node.type === 'TEXT' && node.characters ? node.characters : 'CSS样式预览'}
                    </div>
                    <pre style="margin-top: 10px;">${styles}</pre>
                `
                cssPreview.style.display = 'block'
            } else {
                nodeCssStyles.innerHTML = '<div>该节点没有CSS样式</div>'
                cssPreview.style.display = 'none'
            }
        }

        // 渲染CSS样式
        function renderCssStyles() {
            cssStylesContent.innerHTML = ''

            if (!figmaData.nodes) return

            // 收集所有样式
            const stylesMap = new Map()

            function collectStyles(nodes) {
                if (!Array.isArray(nodes)) return

                nodes.forEach(node => {
                    if (node.cssStyles && Object.keys(node.cssStyles).length > 0) {
                        const styleKey = JSON.stringify(node.cssStyles)

                        if (!stylesMap.has(styleKey)) {
                            stylesMap.set(styleKey, {
                                styles: node.cssStyles,
                                count: 1,
                                nodeName: node.name,
                                nodeType: node.type
                            })
                        } else {
                            const info = stylesMap.get(styleKey)
                            info.count++
                        }
                    }

                    if (node.children) {
                        collectStyles(node.children)
                    }
                })
            }

            collectStyles(figmaData.nodes)

            // 按使用频率排序并仅显示前50个样式
            const sortedStyles = Array.from(stylesMap.entries())
                .sort((a, b) => b[1].count - a[1].count)
                .slice(0, 50)

            // 创建样式卡片
            sortedStyles.forEach(([styleKey, info]) => {
                const { styles, count, nodeName, nodeType } = info

                const card = document.createElement('div')
                card.className = 'style-card'

                let preview = ''
                if (styles.backgroundColor) {
                    preview = `background-color: ${styles.backgroundColor};`
                } else if (styles.color) {
                    preview = `color: ${styles.color}; background-color: #f0f0f0;`
                }

                let stylesStr = ''
                for (const [property, value] of Object.entries(styles)) {
                    stylesStr += `${property}: ${value};\n`
                }

                card.innerHTML = `
                    <div class="style-preview" style="${preview}">
                        <div style="${Object.entries(styles).map(([p, v]) => `${p}: ${v}`).join('; ')}">
                            ${nodeType === 'TEXT' ? '文本样式示例' : '样式预览'}
                        </div>
                    </div>
                    <div class="style-info">
                        <div class="style-name">${nodeName || 'Unnamed'} (${nodeType || 'Unknown'}) - 使用 ${count} 次</div>
                        <div class="style-properties">
                            ${Object.entries(styles).map(([property, value]) => `
                                <div class="property">
                                    ${property.includes('color') || property.includes('background') ?
                        `<span class="color-box" style="background-color: ${value}"></span>` : ''}
                                    <strong>${property}:</strong> ${value}
                                </div>
                            `).join('')}
                        </div>
                    </div>
                `

                cssStylesContent.appendChild(card)
            })
        }
    </script>
</body>

</html>

```

--------------------------------------------------------------------------------
/src/core/parser.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Figma Response Parser
 *
 * Core parsing logic for converting Figma API responses to simplified
 * node structures. Handles node extraction, style processing, and
 * image resource detection.
 *
 * @module core/parser
 */

import type {
  GetFileNodesResponse,
  Node as FigmaDocumentNode,
  GetFileResponse,
} from "@figma/rest-api-spec";
import { generateCSSShorthand } from "~/utils/css.js";
import { isVisible, isVisibleInParent } from "~/utils/validation.js";
import { convertColor, formatRGBAColor } from "~/utils/color.js";
import { isRectangleCornerRadii, hasValue } from "~/utils/validation.js";
import { buildSimplifiedEffects } from "~/core/effects.js";
import { buildSimplifiedStrokes } from "~/core/style.js";
import { generateFileName } from "~/utils/file.js";
import { LayoutOptimizer } from "~/algorithms/layout/optimizer.js";
import { formatPxValue } from "~/utils/css.js";
import { analyzeNodeTree, type FigmaNode } from "~/algorithms/icon/index.js";

import type {
  CSSStyle,
  TextStyle,
  SimplifiedDesign,
  SimplifiedNode,
  ExportInfo,
  ImageResource,
  IconDetectionResult,
} from "~/types/index.js";

// ==================== Node Utilities ====================

/** Node with fill properties */
interface NodeWithFills {
  fills?: Array<{ type: string; imageRef?: string }>;
}

/** Node with styles and children */
interface NodeWithChildren {
  id: string;
  name: string;
  type: string;
  cssStyles?: { backgroundImage?: string; top?: string; left?: string };
  children?: NodeWithChildren[];
  exportInfo?: ExportInfo;
}

/**
 * Check whether the node has an image fill
 */
export function hasImageFill(node: NodeWithFills): boolean {
  return node.fills?.some((fill) => fill.type === "IMAGE" && fill.imageRef) || false;
}

/**
 * Detect and mark image groups
 */
export function detectAndMarkImageGroup(
  node: NodeWithChildren,
  suggestExportFormat: (node: NodeWithChildren) => string,
  generateFileNameFn: (name: string, format: string) => string,
): void {
  // Only handle groups and frames
  if (node.type !== "GROUP" && node.type !== "FRAME") return;

  // Without children it cannot be an image group
  if (!node.children || node.children.length === 0) return;

  // Check whether all children are image types
  const allChildrenAreImages = node.children.every(
    (child) =>
      child.type === "IMAGE" ||
      (child.type === "RECTANGLE" && hasImageFill(child as NodeWithFills)) ||
      (child.type === "ELLIPSE" && hasImageFill(child as NodeWithFills)) ||
      (child.type === "VECTOR" && hasImageFill(child as NodeWithFills)) ||
      (child.type === "FRAME" && child.cssStyles?.backgroundImage),
  );

  // Mark the node as an image group
  if (allChildrenAreImages) {
    const format = suggestExportFormat(node);
    node.exportInfo = {
      type: "IMAGE_GROUP",
      format: format as "PNG" | "JPG" | "SVG",
      nodeId: node.id,
      fileName: generateFileNameFn(node.name, format),
    };

    // Remove child information and export as a whole
    delete node.children;
  }
}

/**
 * Sort nodes by position (top to bottom, left to right)
 */
export function sortNodesByPosition<T extends { cssStyles?: { top?: string; left?: string } }>(
  nodes: T[],
): T[] {
  return [...nodes].sort((a, b) => {
    // Sort by the top value (top to bottom)
    const aTop = a.cssStyles?.top ? parseFloat(a.cssStyles.top) : 0;
    const bTop = b.cssStyles?.top ? parseFloat(b.cssStyles.top) : 0;

    if (aTop !== bTop) {
      return aTop - bTop;
    }

    // When top values are equal, sort by left (left to right)
    const aLeft = a.cssStyles?.left ? parseFloat(a.cssStyles.left) : 0;
    const bLeft = b.cssStyles?.left ? parseFloat(b.cssStyles.left) : 0;
    return aLeft - bLeft;
  });
}

/**
 * Clean up temporary computed properties
 */
export function cleanupTemporaryProperties(node: SimplifiedNode): void {
  // Remove absolute coordinates
  delete node._absoluteX;
  delete node._absoluteY;

  // Recursively clean child nodes
  if (node.children && node.children.length > 0) {
    node.children.forEach(cleanupTemporaryProperties);
  }
}

// ==================== Main Parser ====================

/**
 * Parse Figma API response to simplified design structure
 */
export function parseFigmaResponse(data: GetFileResponse | GetFileNodesResponse): SimplifiedDesign {
  // Extract basic information
  const { name, lastModified, thumbnailUrl } = data;

  // Process nodes
  let nodes: FigmaDocumentNode[] = [];
  if ("document" in data) {
    // If it's a response for the entire file
    nodes = data.document.children;
  } else if ("nodes" in data) {
    // If it's a response for specific nodes
    const nodeData = Object.values(data.nodes).filter(
      (node): node is NonNullable<typeof node> =>
        node !== null && typeof node === "object" && "document" in node,
    );

    nodes = nodeData.map((n) => (n as { document: FigmaDocumentNode }).document);
  }

  // Use the new icon detection algorithm to analyze the node tree
  // Build icon ID map for fast lookup
  const iconMap = new Map<string, IconDetectionResult>();
  for (const node of nodes) {
    const { exportableIcons } = analyzeNodeTree(node as unknown as FigmaNode);
    for (const icon of exportableIcons) {
      iconMap.set(icon.nodeId, icon);
    }
  }

  // Extract nodes and generate simplified data, passing in the icon map
  const simplifiedNodes = extractNodes(nodes, undefined, iconMap);

  // Clean up temporary properties
  simplifiedNodes.forEach(cleanupTemporaryProperties);

  // Apply layout optimization
  const optimizedDesign = LayoutOptimizer.optimizeDesign({
    name,
    lastModified,
    thumbnailUrl: thumbnailUrl || "",
    nodes: simplifiedNodes,
  });

  return optimizedDesign;
}

// ==================== Node Extraction ====================

/**
 * Extract multiple nodes from Figma response
 */
function extractNodes(
  children: FigmaDocumentNode[],
  parentNode?: SimplifiedNode,
  iconMap?: Map<string, IconDetectionResult>,
): SimplifiedNode[] {
  if (!Array.isArray(children)) return [];

  // Create a corresponding original parent node object for visibility judgment
  const parentForVisibility = parentNode
    ? {
        clipsContent: (parentNode as any).clipsContent,
        absoluteBoundingBox:
          parentNode._absoluteX !== undefined && parentNode._absoluteY !== undefined
            ? {
                x: parentNode._absoluteX,
                y: parentNode._absoluteY,
                width: parseFloat(parentNode.cssStyles?.width || "0"),
                height: parseFloat(parentNode.cssStyles?.height || "0"),
              }
            : undefined,
      }
    : undefined;

  const visibilityFilter = (node: FigmaDocumentNode) => {
    // Use type guard to ensure only checking nodes with necessary properties
    const nodeForVisibility = {
      visible: (node as any).visible,
      opacity: (node as any).opacity,
      absoluteBoundingBox: (node as any).absoluteBoundingBox,
      absoluteRenderBounds: (node as any).absoluteRenderBounds,
    };

    // If there's no parent node information, only check the node's own visibility
    if (!parentForVisibility) {
      return isVisible(nodeForVisibility);
    }

    // If there's a parent node, also consider the parent's clipping effect
    return isVisibleInParent(nodeForVisibility, parentForVisibility);
  };

  const nodes = children
    .filter(visibilityFilter)
    .map((node) => extractNode(node, parentNode, iconMap))
    .filter((node): node is SimplifiedNode => node !== null);

  // Sort sibling elements by top value (from top to bottom)
  return sortNodesByPosition(nodes);
}

/**
 * Extract single node information
 * Use the new icon detection algorithm to handle icon merging
 */
function extractNode(
  node: FigmaDocumentNode,
  parentNode?: SimplifiedNode,
  iconMap?: Map<string, IconDetectionResult>,
): SimplifiedNode | null {
  if (!node) return null;

  const { id, name, type } = node;

  // Check if this is an icon node that needs to be exported
  const iconInfo = iconMap?.get(id);
  if (iconInfo && iconInfo.shouldMerge) {
    // This is an icon node, export as a whole, don't process child nodes
    const result: SimplifiedNode = {
      id,
      name,
      type,
    };

    result.cssStyles = {};

    // Add size information
    if (hasValue("absoluteBoundingBox", node) && node.absoluteBoundingBox) {
      result.cssStyles.width = formatPxValue(node.absoluteBoundingBox.width);
      result.cssStyles.height = formatPxValue(node.absoluteBoundingBox.height);

      if ((node.type as string) !== "DOCUMENT" && (node.type as string) !== "CANVAS") {
        result.cssStyles.position = "absolute";
        result._absoluteX = node.absoluteBoundingBox.x;
        result._absoluteY = node.absoluteBoundingBox.y;

        if (
          parentNode &&
          parentNode._absoluteX !== undefined &&
          parentNode._absoluteY !== undefined
        ) {
          result.cssStyles.left = formatPxValue(node.absoluteBoundingBox.x - parentNode._absoluteX);
          result.cssStyles.top = formatPxValue(node.absoluteBoundingBox.y - parentNode._absoluteY);
        } else {
          result.cssStyles.left = formatPxValue(node.absoluteBoundingBox.x);
          result.cssStyles.top = formatPxValue(node.absoluteBoundingBox.y);
        }
      }
    }

    // Set export information
    result.exportInfo = {
      type: "IMAGE",
      format: iconInfo.exportFormat,
      fileName: generateFileName(name, iconInfo.exportFormat),
    };

    // Don't process child nodes, export as a whole image
    return result;
  }

  // Create basic node object
  const result: SimplifiedNode = {
    id,
    name,
    type,
  };

  // Set CSS styles
  result.cssStyles = {};

  // Add CSS conversion logic for size and position
  if (hasValue("absoluteBoundingBox", node) && node.absoluteBoundingBox) {
    // Add to CSS styles (using optimized precision)
    result.cssStyles.width = formatPxValue(node.absoluteBoundingBox.width);
    result.cssStyles.height = formatPxValue(node.absoluteBoundingBox.height);

    // Add positioning information for non-root nodes
    if ((node.type as string) !== "DOCUMENT" && (node.type as string) !== "CANVAS") {
      result.cssStyles.position = "absolute";

      // Store original coordinates for child nodes to calculate relative positions
      result._absoluteX = node.absoluteBoundingBox.x;
      result._absoluteY = node.absoluteBoundingBox.y;

      // If there's a parent node, calculate relative position
      if (
        parentNode &&
        parentNode._absoluteX !== undefined &&
        parentNode._absoluteY !== undefined
      ) {
        result.cssStyles.left = formatPxValue(node.absoluteBoundingBox.x - parentNode._absoluteX);
        result.cssStyles.top = formatPxValue(node.absoluteBoundingBox.y - parentNode._absoluteY);
      } else {
        // Otherwise use absolute position (top-level elements)
        result.cssStyles.left = formatPxValue(node.absoluteBoundingBox.x);
        result.cssStyles.top = formatPxValue(node.absoluteBoundingBox.y);
      }
    }
  }

  // Process text - preserve original text content
  if (hasValue("characters", node) && typeof node.characters === "string") {
    result.text = node.characters;

    // For text nodes, add text color style
    if (hasValue("fills", node) && Array.isArray(node.fills) && node.fills.length > 0) {
      const fill = node.fills[0];
      if (fill.type === "SOLID" && fill.color) {
        // Use convertColor to get hex format color
        const { hex, opacity } = convertColor(fill.color, fill.opacity ?? 1);
        // If opacity is 1, use hex format, otherwise use rgba format
        result.cssStyles.color = opacity === 1 ? hex : formatRGBAColor(fill.color, opacity);
      }
    }
  }

  // Extract image information
  processImageResources(node, result, iconMap);

  // Extract common property processing logic
  processNodeStyle(node, result);
  processFills(node, result);
  processStrokes(node, result);
  processEffects(node, result);
  processCornerRadius(node, result);

  // Recursively process child nodes
  if (hasValue("children", node) && Array.isArray(node.children) && node.children.length) {
    result.children = extractNodes(node.children, result, iconMap);

    // Process image groups (keep original logic for handling image fill cases)
    markImageGroup(result);
  }

  return result;
}

/**
 * Wrapper for detectAndMarkImageGroup with default format suggestion
 */
function markImageGroup(node: SimplifiedNode): void {
  detectAndMarkImageGroup(node, () => "PNG", generateFileName);
}

// ==================== Style Processing ====================

/**
 * Extract image resources from the node
 * Icon export is already handled by iconMap, only process image fills here
 */
function processImageResources(
  node: FigmaDocumentNode,
  result: SimplifiedNode,
  iconMap?: Map<string, IconDetectionResult>,
): void {
  // If already marked as icon export, skip
  if (iconMap?.has(result.id)) {
    return;
  }

  // Check image resources in fills and background
  const imageResources: ImageResource[] = [];

  // Extract image resources from fills
  if (hasValue("fills", node) && Array.isArray(node.fills)) {
    const fillImages = node.fills
      .filter((fill) => fill.type === "IMAGE" && (fill as { imageRef?: string }).imageRef)
      .map((fill) => ({
        imageRef: (fill as { imageRef: string }).imageRef,
      }));

    imageResources.push(...fillImages);
  }

  // Extract image resources from background
  if (hasValue("background", node) && Array.isArray(node.background)) {
    const bgImages = node.background
      .filter((bg) => bg.type === "IMAGE" && (bg as { imageRef?: string }).imageRef)
      .map((bg) => ({
        imageRef: (bg as { imageRef: string }).imageRef,
      }));

    imageResources.push(...bgImages);
  }

  // If image resources are found, save and add export information
  if (imageResources.length > 0) {
    // Set CSS background image property - use the first image
    if (!result.cssStyles) {
      result.cssStyles = {};
    }

    const primaryImage = imageResources[0];
    result.cssStyles.backgroundImage = `url({{FIGMA_IMAGE:${primaryImage.imageRef}}})`;

    // Add export information (omit nodeId as it's the same as node id)
    result.exportInfo = {
      type: "IMAGE",
      format: "PNG",
      // nodeId omitted because it's the same as node id, can be obtained from node id when downloading
      fileName: generateFileName(result.name, "PNG"),
    };
  }
}

/**
 * Process node's style properties
 */
function processNodeStyle(node: FigmaDocumentNode, result: SimplifiedNode): void {
  if (!hasValue("style", node)) return;

  const style = node.style as any;

  // Convert text style
  const textStyle: TextStyle = {
    fontFamily: style?.fontFamily,
    fontSize: style?.fontSize,
    fontWeight: style?.fontWeight,
    textAlignHorizontal: style?.textAlignHorizontal,
    textAlignVertical: style?.textAlignVertical,
  };

  // Process line height
  if (style?.lineHeightPx) {
    const cssStyle = textStyleToCss(textStyle);
    cssStyle.lineHeight = formatPxValue(style.lineHeightPx);
    Object.assign(result.cssStyles!, cssStyle);
  } else {
    Object.assign(result.cssStyles!, textStyleToCss(textStyle));
  }
}

/** Gradient paint type for type narrowing */
interface GradientPaint {
  type: string;
  gradientHandlePositions?: Array<{ x: number; y: number }>;
  gradientStops?: Array<{
    position: number;
    color: { r: number; g: number; b: number; a: number };
  }>;
}

/**
 * Process gradient fills, convert to CSS linear-gradient
 *
 * Figma gradient coordinate system:
 * - Origin (0,0) is at top-left
 * - x-axis points right as positive
 * - y-axis points down as positive
 *
 * CSS gradient angles:
 * - 0deg from bottom to top
 * - 90deg from left to right
 * - 180deg from top to bottom
 * - 270deg from right to left
 */
function processGradient(gradient: GradientPaint): string {
  if (!gradient.gradientHandlePositions || !gradient.gradientStops) return "";

  const stops = gradient.gradientStops
    .map((stop) => {
      const { hex, opacity } = convertColor(stop.color);
      // Use rgba format if alpha < 1, otherwise use hex
      const colorStr = opacity < 1 ? formatRGBAColor(stop.color) : hex;
      return `${colorStr} ${Math.round(stop.position * 100)}%`;
    })
    .join(", ");

  const [start, end] = gradient.gradientHandlePositions;

  // Calculate the angle in Figma (x-axis positive direction is 0 degrees, counter-clockwise is positive)
  const figmaAngle = Math.atan2(end.y - start.y, end.x - start.x) * (180 / Math.PI);

  // Convert to CSS angle:
  // CSS 0deg is upward, rotating clockwise
  // Figma angle needs to add 90 degrees (because Figma 0 degree is rightward, CSS 0 degree is upward)
  const cssAngle = Math.round((figmaAngle + 90 + 360) % 360);

  return `linear-gradient(${cssAngle}deg, ${stops})`;
}

/**
 * Process node's fill properties
 */
function processFills(node: FigmaDocumentNode, result: SimplifiedNode): void {
  if (!hasValue("fills", node) || !Array.isArray(node.fills) || node.fills.length === 0) return;

  // Skip image fills
  if (hasImageFill(node)) {
    return;
  }

  const fills = node.fills.filter(isVisible);
  if (fills.length === 0) return;

  const fill = fills[0];

  if (fill.type === "SOLID" && fill.color) {
    const { hex, opacity } = convertColor(fill.color, fill.opacity ?? 1);
    const color = opacity === 1 ? hex : formatRGBAColor(fill.color, opacity);

    if (node.type === "TEXT") {
      result.cssStyles!.color = color;
    } else {
      result.cssStyles!.backgroundColor = color;
    }
  } else if (fill.type === "GRADIENT_LINEAR") {
    const gradient = processGradient(fill as unknown as GradientPaint);

    if (node.type === "TEXT") {
      result.cssStyles!.background = gradient;
      result.cssStyles!.webkitBackgroundClip = "text";
      result.cssStyles!.backgroundClip = "text";
      result.cssStyles!.webkitTextFillColor = "transparent";
    } else {
      result.cssStyles!.background = gradient;
    }
  }
}

/**
 * Process node's stroke properties
 */
function processStrokes(node: FigmaDocumentNode, result: SimplifiedNode): void {
  if ((node as any).type === "TEXT") return;

  const strokes = buildSimplifiedStrokes(node);
  if (strokes.colors.length === 0) return;

  const stroke = strokes.colors[0];

  // Handle string colors (hex or rgba) - already converted by parsePaint
  if (typeof stroke === "string") {
    result.cssStyles!.borderColor = stroke;
    if (strokes.strokeWeight) {
      result.cssStyles!.borderWidth = strokes.strokeWeight;
    }
    result.cssStyles!.borderStyle = "solid";
  }
  // Handle object fills
  else if (typeof stroke === "object" && "type" in stroke) {
    if (stroke.type === "SOLID" && "color" in stroke) {
      // SimplifiedSolidFill - color is already a string
      result.cssStyles!.borderColor = stroke.color;
      if (strokes.strokeWeight) {
        result.cssStyles!.borderWidth = strokes.strokeWeight;
      }
      result.cssStyles!.borderStyle = "solid";
    } else if (stroke.type === "GRADIENT_LINEAR") {
      // For gradient strokes, we need to build gradient from original data
      // SimplifiedGradientFill doesn't have the raw color data anymore
      // So we use border-image with a simple fallback
      if ("gradientStops" in stroke && stroke.gradientStops && stroke.gradientStops.length > 0) {
        const stops = stroke.gradientStops
          .map((s) => `${s.color} ${Math.round(s.position * 100)}%`)
          .join(", ");
        result.cssStyles!.borderImage = `linear-gradient(90deg, ${stops})`;
        result.cssStyles!.borderImageSlice = "1";
      }
      if (strokes.strokeWeight) {
        result.cssStyles!.borderWidth = strokes.strokeWeight;
      }
    }
  }
}

/**
 * Process node's effects properties
 */
function processEffects(node: FigmaDocumentNode, result: SimplifiedNode): void {
  const effects = buildSimplifiedEffects(node);
  if (effects.boxShadow) result.cssStyles!.boxShadow = effects.boxShadow;
  if (effects.filter) result.cssStyles!.filter = effects.filter;
  if (effects.backdropFilter) result.cssStyles!.backdropFilter = effects.backdropFilter;
}

/**
 * Process node's corner radius properties
 */
function processCornerRadius(node: FigmaDocumentNode, result: SimplifiedNode): void {
  if (!hasValue("cornerRadius", node)) return;

  if (typeof node.cornerRadius === "number" && node.cornerRadius > 0) {
    // Process uniform corner radius (rounded)
    result.cssStyles!.borderRadius = formatPxValue(node.cornerRadius);
  } else if (
    node.cornerRadius === "mixed" &&
    hasValue("rectangleCornerRadii", node, isRectangleCornerRadii)
  ) {
    // Process non-uniform corner radius (top-left, top-right, bottom-right, bottom-left) - rounded
    result.cssStyles!.borderRadius =
      generateCSSShorthand({
        top: Math.round(node.rectangleCornerRadii[0]),
        right: Math.round(node.rectangleCornerRadii[1]),
        bottom: Math.round(node.rectangleCornerRadii[2]),
        left: Math.round(node.rectangleCornerRadii[3]),
      }) || "0";
  }
}

/**
 * Convert text style to CSS style
 * @param textStyle Figma text style
 * @returns CSS style object (default values omitted)
 */
function textStyleToCss(textStyle: TextStyle): CSSStyle {
  const cssStyle: CSSStyle = {};

  if (textStyle.fontFamily) cssStyle.fontFamily = textStyle.fontFamily;
  if (textStyle.fontSize) cssStyle.fontSize = formatPxValue(textStyle.fontSize);

  // fontWeight: omit default value 400
  if (textStyle.fontWeight && textStyle.fontWeight !== 400) {
    cssStyle.fontWeight = textStyle.fontWeight;
  }

  // Process text alignment (omit default value 'left')
  if (textStyle.textAlignHorizontal) {
    switch (textStyle.textAlignHorizontal) {
      case "LEFT":
        // Omit default value
        break;
      case "CENTER":
        cssStyle.textAlign = "center";
        break;
      case "RIGHT":
        cssStyle.textAlign = "right";
        break;
      case "JUSTIFIED":
        cssStyle.textAlign = "justify";
        break;
    }
  }

  // Process vertical alignment (omit default value 'top')
  if (textStyle.textAlignVertical) {
    switch (textStyle.textAlignVertical) {
      case "TOP":
        // Omit default value
        break;
      case "CENTER":
        cssStyle.verticalAlign = "middle";
        break;
      case "BOTTOM":
        cssStyle.verticalAlign = "bottom";
        break;
    }
  }

  return cssStyle;
}

```

--------------------------------------------------------------------------------
/tests/fixtures/expected/node-402-34955-optimized.json:
--------------------------------------------------------------------------------

```json
{
  "name": "Untitled",
  "lastModified": "2025-12-05T16:16:42Z",
  "thumbnailUrl": "https://s3-alpha.figma.com/thumbnails/6d4c4440-1098-402c-b983-1bf44c182966?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAQ4GOSFWCXJW6HYPC%2F20251204%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251204T000000Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=d3f06b6fe775803929be1e1b720481975e85de86c48cc9eceead613e35ad8501",
  "nodes": [
    {
      "id": "402:34955",
      "name": "Group 1410104853",
      "type": "GROUP",
      "cssStyles": {
        "width": "1580px",
        "height": "895px",
        "position": "absolute",
        "left": "29147px",
        "top": "88511px"
      },
      "children": [
        {
          "id": "398:31938",
          "name": "Rectangle 34",
          "type": "RECTANGLE",
          "cssStyles": {
            "width": "1580px",
            "height": "895px",
            "position": "absolute",
            "left": "0px",
            "top": "0px",
            "backgroundColor": "#FFFFFF",
            "borderRadius": "12px"
          }
        },
        {
          "id": "398:34954",
          "name": "Group 1410104852",
          "type": "GROUP",
          "cssStyles": {
            "width": "1580px",
            "height": "340px",
            "position": "absolute",
            "left": "0px",
            "top": "19px",
            "display": "grid",
            "gridTemplateColumns": "500px 500px 500px",
            "gap": "12px",
            "padding": "170px 26px 0px"
          },
          "children": [
            {
              "id": "398:31982",
              "name": "Group 1410104526",
              "type": "GROUP",
              "cssStyles": {
                "width": "320px",
                "height": "41px",
                "position": "absolute",
                "left": "630px",
                "top": "0px",
                "display": "flex",
                "justifyContent": "space-between",
                "alignItems": "flex-start"
              },
              "children": [
                {
                  "id": "398:31983",
                  "name": "Group 1410104524",
                  "type": "GROUP",
                  "cssStyles": {
                    "width": "160px",
                    "height": "41px",
                    "display": "flex",
                    "flexDirection": "column",
                    "gap": "16px",
                    "justifyContent": "space-between",
                    "alignItems": "center"
                  },
                  "children": [
                    {
                      "id": "398:31984",
                      "name": "Add Keywords",
                      "type": "TEXT",
                      "cssStyles": {
                        "width": "111px",
                        "height": "22px",
                        "color": "#009663",
                        "fontFamily": "PingFang SC",
                        "fontSize": "16px",
                        "fontWeight": 600,
                        "textAlign": "center",
                        "verticalAlign": "middle",
                        "lineHeight": "22px"
                      },
                      "text": "Add Keywords"
                    },
                    {
                      "id": "398:31985",
                      "name": "Rectangle 34626003",
                      "type": "RECTANGLE",
                      "cssStyles": {
                        "width": "160px",
                        "height": "3px",
                        "backgroundColor": "#24C790"
                      }
                    }
                  ]
                },
                {
                  "id": "398:31986",
                  "name": "Group 1410104525",
                  "type": "GROUP",
                  "cssStyles": {
                    "width": "160px",
                    "height": "41px",
                    "display": "flex",
                    "flexDirection": "column",
                    "gap": "16px",
                    "justifyContent": "space-between",
                    "alignItems": "center"
                  },
                  "children": [
                    {
                      "id": "398:31987",
                      "name": "Detected Alerts",
                      "type": "TEXT",
                      "cssStyles": {
                        "width": "116px",
                        "height": "22px",
                        "color": "#666666",
                        "fontFamily": "PingFang SC",
                        "fontSize": "16px",
                        "textAlign": "center",
                        "verticalAlign": "middle",
                        "lineHeight": "22px"
                      },
                      "text": "Detected Alerts"
                    },
                    {
                      "id": "398:31988",
                      "name": "Rectangle 34626001",
                      "type": "RECTANGLE",
                      "cssStyles": {
                        "width": "160px",
                        "height": "3px",
                        "backgroundColor": "#FFFFFF"
                      }
                    }
                  ]
                }
              ]
            },
            {
              "id": "398:31981",
              "name": "Vector 512 (Stroke)",
              "type": "VECTOR",
              "cssStyles": {
                "width": "1580px",
                "height": "1px",
                "position": "absolute",
                "left": "0px",
                "top": "41px",
                "backgroundColor": "rgba(66, 149, 136, 0.2)"
              }
            },
            {
              "id": "398:31966",
              "name": "Group 1410104484",
              "type": "GROUP",
              "cssStyles": {
                "width": "1528px",
                "height": "88px",
                "position": "absolute",
                "left": "26px",
                "top": "62px",
                "backgroundColor": "#F4F3F6",
                "borderRadius": "16px"
              },
              "children": [
                {
                  "id": "398:31968",
                  "name": "Group 1410104477",
                  "type": "GROUP",
                  "cssStyles": {
                    "width": "109px",
                    "height": "22px",
                    "position": "absolute",
                    "left": "24px",
                    "top": "18px",
                    "borderRadius": "3px",
                    "display": "flex",
                    "gap": "8px",
                    "justifyContent": "space-between",
                    "alignItems": "flex-start"
                  },
                  "children": [
                    {
                      "id": "398:31970",
                      "name": "已添加关键词",
                      "type": "TEXT",
                      "cssStyles": {
                        "width": "96px",
                        "height": "22px",
                        "color": "#000000",
                        "fontFamily": "PingFang SC",
                        "fontSize": "16px",
                        "fontWeight": 600,
                        "verticalAlign": "middle",
                        "lineHeight": "22px"
                      },
                      "text": "已添加关键词"
                    },
                    {
                      "id": "398:31969",
                      "name": "Rectangle 34626011",
                      "type": "RECTANGLE",
                      "cssStyles": {
                        "width": "5px",
                        "height": "18px",
                        "backgroundColor": "#24C790",
                        "borderRadius": "3px"
                      }
                    }
                  ]
                },
                {
                  "id": "398:31972",
                  "name": "4/20",
                  "type": "TEXT",
                  "cssStyles": {
                    "width": "33px",
                    "height": "20px",
                    "position": "absolute",
                    "left": "141px",
                    "top": "19px",
                    "color": "#999999",
                    "fontFamily": "PingFang SC",
                    "fontSize": "14px",
                    "verticalAlign": "middle",
                    "lineHeight": "20px"
                  },
                  "text": "4/20"
                },
                {
                  "id": "398:31978",
                  "name": "Link",
                  "type": "FRAME",
                  "cssStyles": {
                    "width": "200px",
                    "height": "42px",
                    "position": "absolute",
                    "left": "1108px",
                    "top": "23px",
                    "backgroundColor": "#FFFFFF",
                    "borderRadius": "8px"
                  },
                  "children": [
                    {
                      "id": "398:31980",
                      "name": "Container",
                      "type": "FRAME",
                      "cssStyles": {
                        "width": "200px",
                        "height": "42px",
                        "position": "absolute",
                        "left": "0px",
                        "top": "0px",
                        "borderRadius": "8px"
                      }
                    },
                    {
                      "id": "398:31979",
                      "name": "添加自定义关键词",
                      "type": "TEXT",
                      "cssStyles": {
                        "width": "115px",
                        "height": "16px",
                        "position": "absolute",
                        "left": "43px",
                        "top": "13px",
                        "color": "#333333",
                        "fontFamily": "Roboto",
                        "fontSize": "14px",
                        "textAlign": "center",
                        "verticalAlign": "middle",
                        "lineHeight": "16px"
                      },
                      "text": "添加自定义关键词"
                    }
                  ]
                },
                {
                  "id": "398:31973",
                  "name": "Link",
                  "type": "FRAME",
                  "cssStyles": {
                    "width": "170px",
                    "height": "42px",
                    "position": "absolute",
                    "left": "1328px",
                    "top": "23px",
                    "backgroundColor": "#24C790",
                    "borderRadius": "8px"
                  },
                  "children": [
                    {
                      "id": "398:31974",
                      "name": "Group 1410104508",
                      "type": "GROUP",
                      "cssStyles": {
                        "width": "112px",
                        "height": "20px",
                        "position": "absolute",
                        "left": "29px",
                        "top": "10px",
                        "display": "flex",
                        "gap": "6px",
                        "justifyContent": "space-between",
                        "alignItems": "flex-start"
                      },
                      "children": [
                        {
                          "id": "398:31976",
                          "name": "Frame",
                          "type": "FRAME",
                          "cssStyles": {
                            "width": "20px",
                            "height": "20px"
                          },
                          "exportInfo": {
                            "type": "IMAGE",
                            "format": "SVG",
                            "fileName": "frame.svg"
                          }
                        },
                        {
                          "id": "398:31975",
                          "name": "AI生成关键词",
                          "type": "TEXT",
                          "cssStyles": {
                            "width": "86px",
                            "height": "16px",
                            "color": "#FFFFFF",
                            "fontFamily": "Roboto",
                            "fontSize": "14px",
                            "textAlign": "center",
                            "verticalAlign": "middle",
                            "lineHeight": "16px"
                          },
                          "text": "AI生成关键词"
                        }
                      ]
                    }
                  ]
                },
                {
                  "id": "398:31971",
                  "name": "添加自定义关键词,启动分类即可检测分类下的关键词。",
                  "type": "TEXT",
                  "cssStyles": {
                    "width": "350px",
                    "height": "20px",
                    "position": "absolute",
                    "left": "24px",
                    "top": "50px",
                    "color": "#999999",
                    "fontFamily": "PingFang SC",
                    "fontSize": "14px",
                    "verticalAlign": "middle",
                    "lineHeight": "20px"
                  },
                  "text": "添加自定义关键词,启动分类即可检测分类下的关键词。"
                }
              ]
            },
            {
              "id": "398:31946",
              "name": "Group 1410104481",
              "type": "COMPONENT",
              "cssStyles": {
                "width": "500px",
                "height": "78px",
                "backgroundColor": "#FFFFFF",
                "borderRadius": "16px",
                "borderWidth": "1px",
                "borderStyle": "solid",
                "borderColor": "#C4C4C4"
              },
              "children": [
                {
                  "id": "398:31950",
                  "name": "Frame 1410086320",
                  "type": "FRAME",
                  "cssStyles": {
                    "width": "70px",
                    "height": "42px",
                    "position": "absolute",
                    "left": "22px",
                    "top": "18px",
                    "display": "flex",
                    "flexDirection": "column",
                    "gap": "4px",
                    "justifyContent": "space-between",
                    "alignItems": "flex-start"
                  },
                  "children": [
                    {
                      "id": "398:31951",
                      "name": "Frame 1410086319",
                      "type": "FRAME",
                      "cssStyles": {
                        "width": "70px",
                        "height": "20px",
                        "display": "flex",
                        "gap": "10px",
                        "justifyContent": "space-between",
                        "alignItems": "center",
                        "padding": "0px 28px 0px 0px"
                      },
                      "children": [
                        {
                          "id": "398:31952",
                          "name": "早恋",
                          "type": "TEXT",
                          "cssStyles": {
                            "width": "28px",
                            "height": "20px",
                            "color": "#000000",
                            "fontFamily": "PingFang SC",
                            "fontSize": "14px",
                            "fontWeight": 500,
                            "verticalAlign": "middle",
                            "lineHeight": "20px"
                          },
                          "text": "早恋"
                        },
                        {
                          "id": "398:31953",
                          "name": "Vector 585",
                          "type": "VECTOR",
                          "cssStyles": {
                            "width": "4px",
                            "height": "8px"
                          },
                          "exportInfo": {
                            "type": "IMAGE",
                            "format": "SVG",
                            "fileName": "vector_585.svg"
                          }
                        }
                      ]
                    },
                    {
                      "id": "398:31954",
                      "name": "8 个关键词",
                      "type": "TEXT",
                      "cssStyles": {
                        "width": "70px",
                        "height": "17px",
                        "color": "#999999",
                        "fontFamily": "PingFang SC",
                        "fontSize": "12px",
                        "verticalAlign": "middle",
                        "lineHeight": "17px"
                      },
                      "text": "8 个关键词"
                    }
                  ]
                },
                {
                  "id": "398:31948",
                  "name": "Frame",
                  "type": "FRAME",
                  "cssStyles": {
                    "width": "34px",
                    "height": "34px",
                    "position": "absolute",
                    "left": "442px",
                    "top": "22px"
                  },
                  "exportInfo": {
                    "type": "IMAGE",
                    "format": "SVG",
                    "fileName": "frame.svg"
                  }
                }
              ]
            },
            {
              "id": "398:31956",
              "name": "Group 1410104482",
              "type": "FRAME",
              "cssStyles": {
                "width": "500px",
                "height": "78px",
                "backgroundColor": "#FFFFFF",
                "borderRadius": "16px",
                "borderWidth": "1px",
                "borderStyle": "solid",
                "borderColor": "#C4C4C4"
              },
              "children": [
                {
                  "id": "398:31960",
                  "name": "Frame 1410086320",
                  "type": "FRAME",
                  "cssStyles": {
                    "width": "70px",
                    "height": "42px",
                    "position": "absolute",
                    "left": "22px",
                    "top": "18px",
                    "display": "flex",
                    "flexDirection": "column",
                    "gap": "4px",
                    "justifyContent": "space-between",
                    "alignItems": "flex-start"
                  },
                  "children": [
                    {
                      "id": "398:31961",
                      "name": "Frame 1410086319",
                      "type": "FRAME",
                      "cssStyles": {
                        "width": "70px",
                        "height": "20px",
                        "display": "flex",
                        "gap": "10px",
                        "justifyContent": "space-between",
                        "alignItems": "center"
                      },
                      "children": [
                        {
                          "id": "398:31962",
                          "name": "早恋",
                          "type": "TEXT",
                          "cssStyles": {
                            "width": "56px",
                            "height": "20px",
                            "color": "#000000",
                            "fontFamily": "PingFang SC",
                            "fontSize": "14px",
                            "fontWeight": 500,
                            "verticalAlign": "middle",
                            "lineHeight": "20px"
                          },
                          "text": "校园暴力"
                        },
                        {
                          "id": "398:31963",
                          "name": "Vector 585",
                          "type": "VECTOR",
                          "cssStyles": {
                            "width": "4px",
                            "height": "8px"
                          },
                          "exportInfo": {
                            "type": "IMAGE",
                            "format": "SVG",
                            "fileName": "vector_585.svg"
                          }
                        }
                      ]
                    },
                    {
                      "id": "398:31964",
                      "name": "8 个关键词",
                      "type": "TEXT",
                      "cssStyles": {
                        "width": "70px",
                        "height": "17px",
                        "color": "#999999",
                        "fontFamily": "PingFang SC",
                        "fontSize": "12px",
                        "verticalAlign": "middle",
                        "lineHeight": "17px"
                      },
                      "text": "67 个关键词"
                    }
                  ]
                },
                {
                  "id": "398:31958",
                  "name": "Frame",
                  "type": "FRAME",
                  "cssStyles": {
                    "width": "34px",
                    "height": "34px",
                    "position": "absolute",
                    "left": "442px",
                    "top": "22px"
                  },
                  "exportInfo": {
                    "type": "IMAGE",
                    "format": "SVG",
                    "fileName": "frame.svg"
                  }
                }
              ]
            },
            {
              "id": "398:31965",
              "name": "Group 1410104483",
              "type": "INSTANCE",
              "cssStyles": {
                "width": "500px",
                "height": "78px",
                "backgroundColor": "#FFFFFF",
                "borderRadius": "16px",
                "borderWidth": "1px",
                "borderStyle": "solid",
                "borderColor": "#C4C4C4"
              },
              "children": [
                {
                  "id": "I398:31965;398:31950",
                  "name": "Frame 1410086320",
                  "type": "FRAME",
                  "cssStyles": {
                    "width": "70px",
                    "height": "42px",
                    "position": "absolute",
                    "left": "22px",
                    "top": "18px",
                    "display": "flex",
                    "flexDirection": "column",
                    "gap": "4px",
                    "justifyContent": "space-between",
                    "alignItems": "flex-start"
                  },
                  "children": [
                    {
                      "id": "I398:31965;398:31951",
                      "name": "Frame 1410086319",
                      "type": "FRAME",
                      "cssStyles": {
                        "width": "70px",
                        "height": "20px",
                        "display": "flex",
                        "gap": "10px",
                        "justifyContent": "space-between",
                        "alignItems": "center"
                      },
                      "children": [
                        {
                          "id": "I398:31965;398:31952",
                          "name": "早恋",
                          "type": "TEXT",
                          "cssStyles": {
                            "width": "56px",
                            "height": "20px",
                            "color": "#000000",
                            "fontFamily": "PingFang SC",
                            "fontSize": "14px",
                            "fontWeight": 500,
                            "verticalAlign": "middle",
                            "lineHeight": "20px"
                          },
                          "text": "露骨低俗"
                        },
                        {
                          "id": "I398:31965;398:31953",
                          "name": "Vector 585",
                          "type": "VECTOR",
                          "cssStyles": {
                            "width": "4px",
                            "height": "8px"
                          },
                          "exportInfo": {
                            "type": "IMAGE",
                            "format": "SVG",
                            "fileName": "vector_585.svg"
                          }
                        }
                      ]
                    },
                    {
                      "id": "I398:31965;398:31954",
                      "name": "8 个关键词",
                      "type": "TEXT",
                      "cssStyles": {
                        "width": "70px",
                        "height": "17px",
                        "color": "#999999",
                        "fontFamily": "PingFang SC",
                        "fontSize": "12px",
                        "verticalAlign": "middle",
                        "lineHeight": "17px"
                      },
                      "text": "13 个关键词"
                    }
                  ]
                },
                {
                  "id": "I398:31965;398:31948",
                  "name": "Frame",
                  "type": "FRAME",
                  "cssStyles": {
                    "width": "34px",
                    "height": "34px",
                    "position": "absolute",
                    "left": "442px",
                    "top": "22px"
                  },
                  "exportInfo": {
                    "type": "IMAGE",
                    "format": "SVG",
                    "fileName": "frame.svg"
                  }
                }
              ]
            },
            {
              "id": "398:31955",
              "name": "Group 1410104485",
              "type": "INSTANCE",
              "cssStyles": {
                "width": "500px",
                "height": "78px",
                "backgroundColor": "#FFFFFF",
                "borderRadius": "16px",
                "borderWidth": "1px",
                "borderStyle": "solid",
                "borderColor": "#C4C4C4"
              },
              "children": [
                {
                  "id": "I398:31955;398:31950",
                  "name": "Frame 1410086320",
                  "type": "FRAME",
                  "cssStyles": {
                    "width": "70px",
                    "height": "42px",
                    "position": "absolute",
                    "left": "22px",
                    "top": "18px",
                    "display": "flex",
                    "flexDirection": "column",
                    "gap": "4px",
                    "justifyContent": "space-between",
                    "alignItems": "flex-start"
                  },
                  "children": [
                    {
                      "id": "I398:31955;398:31951",
                      "name": "Frame 1410086319",
                      "type": "FRAME",
                      "cssStyles": {
                        "width": "70px",
                        "height": "20px",
                        "display": "flex",
                        "gap": "10px",
                        "justifyContent": "space-between",
                        "alignItems": "center"
                      },
                      "children": [
                        {
                          "id": "I398:31955;398:31952",
                          "name": "早恋",
                          "type": "TEXT",
                          "cssStyles": {
                            "width": "56px",
                            "height": "20px",
                            "color": "#000000",
                            "fontFamily": "PingFang SC",
                            "fontSize": "14px",
                            "fontWeight": 500,
                            "verticalAlign": "middle",
                            "lineHeight": "20px"
                          },
                          "text": "色情违法"
                        },
                        {
                          "id": "I398:31955;398:31953",
                          "name": "Vector 585",
                          "type": "VECTOR",
                          "cssStyles": {
                            "width": "4px",
                            "height": "8px"
                          },
                          "exportInfo": {
                            "type": "IMAGE",
                            "format": "SVG",
                            "fileName": "vector_585.svg"
                          }
                        }
                      ]
                    },
                    {
                      "id": "I398:31955;398:31954",
                      "name": "8 个关键词",
                      "type": "TEXT",
                      "cssStyles": {
                        "width": "70px",
                        "height": "17px",
                        "color": "#999999",
                        "fontFamily": "PingFang SC",
                        "fontSize": "12px",
                        "verticalAlign": "middle",
                        "lineHeight": "17px"
                      },
                      "text": "32 个关键词"
                    }
                  ]
                },
                {
                  "id": "I398:31955;398:31948",
                  "name": "Frame",
                  "type": "FRAME",
                  "cssStyles": {
                    "width": "34px",
                    "height": "34px",
                    "position": "absolute",
                    "left": "442px",
                    "top": "22px"
                  },
                  "exportInfo": {
                    "type": "IMAGE",
                    "format": "SVG",
                    "fileName": "frame.svg"
                  }
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

```

--------------------------------------------------------------------------------
/docs/en/layout-detection-research.md:
--------------------------------------------------------------------------------

```markdown
# Design-to-Code Layout Detection Research

## Executive Summary

This document provides a comprehensive analysis of layout detection algorithms used in design-to-code (D2C) conversion tools. The research focuses on how various implementations distinguish between flow (flex) layouts, stacked (absolute) layouts, and grid layouts.

## Table of Contents

1. [Problem Definition](#problem-definition)
2. [Industry Implementations](#industry-implementations)
3. [Academic Research](#academic-research)
4. [Algorithm Comparison](#algorithm-comparison)
5. [Detection Criteria & Thresholds](#detection-criteria--thresholds)
6. [Overlap Detection](#overlap-detection)
7. [Implementation Recommendations](#implementation-recommendations)

---

## Problem Definition

### The Core Challenge

Design tools like Figma store element positions as absolute coordinates (x, y, width, height). Converting these to semantic CSS layouts requires inferring the designer's intent:

| Layout Type           | CSS Property                            | Use Case                                     |
| --------------------- | --------------------------------------- | -------------------------------------------- |
| **Flow (Horizontal)** | `display: flex; flex-direction: row`    | Elements in a row with consistent spacing    |
| **Flow (Vertical)**   | `display: flex; flex-direction: column` | Elements stacked vertically                  |
| **Grid**              | `display: grid`                         | 2D matrix of aligned elements                |
| **Stacked/Absolute**  | `position: absolute`                    | Overlapping elements, decorative positioning |

### Key Questions to Answer

1. How do we detect if elements form a **row** vs a **column**?
2. When should we use **Grid** instead of **nested Flex**?
3. How do we identify **overlapping/stacked** elements that need absolute positioning?
4. What **tolerance thresholds** work best for real-world designs?

---

## Industry Implementations

### 1. imgcook (Alibaba D2C Platform)

**Architecture**: Deterministic-first pipeline with ML fallback

```
Design JSON → Flatten → Row/Column Grouping → Layout Inference → Semantic Labels → Code
```

**Flow vs Stacked Detection Algorithm**:

```typescript
// Core concept: Axis overlap determines layout type
function detectLayoutDirection(elements: Element[]): 'row' | 'column' | 'stacked' {
  // Step 1: Check Y-axis overlap (same horizontal row)
  const yOverlapGroups = groupByAxisOverlap(elements, 'y', tolerance: 2);

  // Step 2: If only one Y-group, check X-axis overlap (vertical column)
  if (yOverlapGroups.length === 1) {
    const xOverlapGroups = groupByAxisOverlap(elements, 'x', tolerance: 2);
    if (xOverlapGroups.length > 1) return 'column';
  }

  // Step 3: Check for overlapping elements
  const hasOverlap = elements.some((a, i) =>
    elements.slice(i + 1).some(b => calculateIoU(a, b) > 0.1)
  );

  return hasOverlap ? 'stacked' : 'row';
}
```

**Grouping Algorithm**:

```typescript
function groupByAxisOverlap(elements: Element[], axis: "x" | "y", tolerance: number): Element[][] {
  // Sort by axis position
  const sorted = elements.sort((a, b) => a[axis] - b[axis]);
  const groups: Element[][] = [[sorted[0]]];

  for (let i = 1; i < sorted.length; i++) {
    const current = sorted[i];
    const lastGroup = groups[groups.length - 1];
    const lastElement = lastGroup[lastGroup.length - 1];

    // Check if current overlaps with last element on opposite axis
    const overlaps =
      axis === "y"
        ? rangesOverlap(lastElement.y, lastElement.bottom, current.y, current.bottom, tolerance)
        : rangesOverlap(lastElement.x, lastElement.right, current.x, current.right, tolerance);

    if (overlaps) {
      lastGroup.push(current);
    } else {
      groups.push([current]);
    }
  }

  return groups;
}
```

**Key Thresholds**:

| Parameter             | Value    | Purpose                       |
| --------------------- | -------- | ----------------------------- |
| Y-axis tolerance      | 2px      | Row grouping                  |
| X-axis tolerance      | 2px      | Column grouping               |
| Gap variance          | 20%      | Detect consistent spacing     |
| Alignment tolerance   | 2px      | Detect aligned edges          |
| IoU overlap threshold | 0.1      | Mark for absolute positioning |
| Max recursion depth   | 5        | Prevent infinite nesting      |
| Gap rounding          | 4px grid | Snap to common design values  |

**Confidence Scoring**:

```typescript
interface LayoutConfidence {
  patternCoverage: number; // How many elements fit the pattern
  gapConsistency: number; // Standard deviation of gaps
  alignmentAccuracy: number; // How well elements align
}

function calculateConfidence(analysis: LayoutAnalysis): number {
  let score = 0;

  score += analysis.patternCoverage * 0.4; // 40% weight
  score += analysis.gapConsistency * 0.35; // 35% weight
  score += analysis.alignmentAccuracy * 0.25; // 25% weight

  return score;
}

// Only accept layout if confidence >= 0.3
const MIN_CONFIDENCE = 0.3;
```

---

### 2. FigmaToCode (Open Source)

**Approach**: Metadata-first (trusts Figma Auto Layout)

```typescript
// FigmaToCode does NOT infer layout from coordinates
// Instead, it reads Figma's native Auto Layout metadata
function convertLayout(node: FigmaNode): CSSLayout {
  if (node.layoutMode === "HORIZONTAL") {
    return { display: "flex", flexDirection: "row" };
  }
  if (node.layoutMode === "VERTICAL") {
    return { display: "flex", flexDirection: "column" };
  }
  // No Auto Layout = fall back to absolute positioning
  return { position: "absolute" };
}
```

**Limitations**:

- Requires designers to use Figma Auto Layout feature
- Legacy designs without Auto Layout get absolute positioning
- No coordinate-based layout inference

**Key Innovation**: AltNodes intermediate representation

- Converts Figma nodes to simplified virtual nodes
- Handles mixed positioning (absolute + auto-layout children)
- Intelligent decisions about code structure

---

### 3. Locofy

**Approach**: Auto Layout + Absolute positioning hybrid

Locofy uses LocoAI to analyze designs and apply appropriate CSS properties:

```typescript
// Locofy's approach to layout detection
function analyzeLayout(elements: Element[]): LayoutDecision {
  // LocoAI groups elements for better structure
  const groups = locoAI.groupElements(elements);

  // Apply relevant CSS property (flex) for responsiveness
  for (const group of groups) {
    if (group.hasAutoLayout) {
      // Auto layout corresponds to Flexbox in CSS
      applyFlexLayout(group);
    } else if (group.isFloating) {
      // Floating elements use absolute positioning
      applyAbsolutePosition(group);
    }
  }
}
```

**Key Features**:

- Change absolute position status and re-run algorithm for regrouping
- Floating elements use absolute property in auto layout setting
- Parent of absolutely positioned element determines positioning context

---

### 4. Anima Auto-Flexbox

**Approach**: Computer Vision algorithms for automatic Flexbox

```typescript
// Anima's Auto-Flexbox algorithm
// Reverse-engineered from developer thought process
function applyAutoFlexbox(design: Design): Layout {
  // Without Auto-Flexbox: generates absolute layout
  // With Auto-Flexbox: generates relative positioning (Flexbox)

  // Computer Vision algorithms from CV world
  const flexboxLayout = cvAlgorithm.analyzeAndApply(design);

  // Relative positioning allows layers to push each other
  return flexboxLayout;
}
```

**Key Insight**: "Absolute layout is great for design phase, but less so for end product. Flexbox layout means relative positioning."

---

### 5. Gridaco / Grida

**Approach**: Rules + ML hybrid (similar to imgcook)

```typescript
// High-availability layout direction via heuristics
// ML reserved for component/loop recognition
function detectLayout(elements: Element[]): LayoutType {
  // Rule-based direction detection first
  const direction = detectDirectionByRules(elements);

  // ML for semantic understanding
  const semantics = mlModel.predictSemantics(elements);

  return combineResults(direction, semantics);
}
```

---

### 6. Phoenix Codie Position Detection System

**Key Innovation**: Explicit "position: absolute abuse" avoidance

```typescript
interface PositionDecision {
  element: Element;
  shouldBeAbsolute: boolean;
  reason: "overlap" | "decorative" | "anchor" | "flow";
}

function analyzePosition(element: Element, siblings: Element[]): PositionDecision {
  // Only use absolute for:
  // 1. Overlapping elements
  // 2. Decorative elements (badges, icons positioned freely)
  // 3. Anchor elements (tooltips, modals)

  const overlaps = siblings.some((s) => hasSignificantOverlap(element, s));
  const isDecorative = element.type === "DECORATIVE";
  const isAnchor = element.hasAnchorConstraint;

  return {
    element,
    shouldBeAbsolute: overlaps || isDecorative || isAnchor,
    reason: overlaps ? "overlap" : isDecorative ? "decorative" : isAnchor ? "anchor" : "flow",
  };
}
```

---

## Academic Research

### 1. Allen's Interval Algebra (Foundational)

**Source**: "A Layout Inference Algorithm for GUIs" (ScienceDirect)

Allen's 13 interval relations describe 1D spatial relationships:

```
| Relation | Visual | Description |
|----------|--------|-------------|
| before   | A---B  | A completely before B |
| meets    | A--B   | A ends where B starts |
| overlaps | A-B-   | A partially overlaps B |
| starts   | AB--   | A starts at same position as B |
| during   | -A-B   | A completely inside B |
| equals   | A=B    | Same position and size |
```

**Application to Layout Detection**:

```typescript
function getAllenRelation(a: Interval, b: Interval): AllenRelation {
  if (a.end < b.start) return "before";
  if (a.end === b.start) return "meets";
  if (a.start < b.start && a.end > b.start && a.end < b.end) return "overlaps";
  if (a.start === b.start && a.end < b.end) return "starts";
  if (a.start > b.start && a.end < b.end) return "during";
  if (a.start === b.start && a.end === b.end) return "equals";
  // ... inverse relations
}

// For 2D layouts, apply Allen relations to both X and Y axes
function get2DRelation(a: Rect, b: Rect): [AllenRelation, AllenRelation] {
  return [
    getAllenRelation({ start: a.x, end: a.right }, { start: b.x, end: b.right }),
    getAllenRelation({ start: a.y, end: a.bottom }, { start: b.y, end: b.bottom }),
  ];
}
```

**Two-Phase Algorithm**:

1. **Phase 1**: Convert absolute coordinates to relative positioning using directed graphs
2. **Phase 2**: Apply pattern matching and graph rewriting for layout composition

**Results**: 97% layout faithfulness, 84% proportional retention on resize

---

### 2. GRIDS - MILP-Based Layout Inference (Aalto University)

**Approach**: Mathematical optimization for grid generation

```python
# Formulate grid inference as Mixed Integer Linear Programming
def solve_grid_layout(elements):
    model = gp.Model("grid_layout")

    # Variables: track sizes, element placements
    track_widths = model.addVars(max_columns, lb=0, name="col_width")
    track_heights = model.addVars(max_rows, lb=0, name="row_height")
    placements = model.addVars(len(elements), max_rows, max_columns, vtype=GRB.BINARY)

    # Constraints: elements must fit in tracks, no overlap
    for i, elem in enumerate(elements):
        # Element width must equal sum of spanned track widths
        model.addConstr(sum(track_widths[c] * placements[i,r,c]
                           for r in range(max_rows)
                           for c in range(max_columns)) >= elem.width)

    # Objective: minimize deviation from original positions
    model.setObjective(sum_position_errors, GRB.MINIMIZE)

    model.optimize()
    return extract_grid_template(model)
```

**Advantages**:

- Mathematically optimal solutions
- Handles complex grid configurations
- Precise track size calculation

**Disadvantages**:

- Computationally expensive
- Requires Gurobi optimizer
- Overkill for simple layouts

---

### 3. UI Semantic Group Detection (arXiv 2024)

**Key Innovation**: Gestalt-based pre-clustering

```typescript
// Apply Gestalt principles before layout detection
interface GestaltAnalysis {
  proximityGroups: Element[][];     // Close elements grouped
  similarityGroups: Element[][];    // Similar-looking elements grouped
  continuityChains: Element[][];    // Elements forming visual lines
  closureGroups: Element[][];       // Elements forming enclosed shapes
}

function applyGestaltPrinciples(elements: Element[]): GestaltAnalysis {
  return {
    proximityGroups: clusterByProximity(elements, distanceThreshold: 20),
    similarityGroups: clusterBySimilarity(elements, sizeVariance: 0.2),
    continuityChains: detectVisualLines(elements),
    closureGroups: detectEnclosures(elements)
  };
}

// Use Gestalt groups to improve grid detection
function detectGridWithGestalt(elements: Element[]): GridResult {
  const gestalt = applyGestaltPrinciples(elements);

  // Only consider similarity groups for grid detection
  for (const group of gestalt.similarityGroups) {
    if (group.length >= 4) {
      const gridResult = detectGridLayout(group);
      if (gridResult.confidence >= 0.6) {
        return gridResult;
      }
    }
  }

  return { isGrid: false };
}
```

---

### 4. Screen Parsing (CMU UIST 2021)

**Approach**: ML-based container type prediction

- Trained on 210K mobile screens (130K iOS + 80K Android)
- Predicts 7 container types including grids
- Uses visual features and spatial relationships

```typescript
interface ContainerPrediction {
  type: "list" | "grid" | "carousel" | "tabs" | "form" | "navigation" | "content";
  confidence: number;
  children: Element[];
}

// ML model input features
interface ScreenFeatures {
  elementBoundingBoxes: Rect[];
  elementTypes: string[];
  visualSimilarities: number[][]; // Pairwise visual similarity
  spatialRelations: AllenRelation[][]; // Pairwise spatial relations
}
```

---

## Algorithm Comparison

### Detection Approaches Summary

| Tool/Research         | Approach          | Flow Detection          | Grid Detection             | Overlap Handling            |
| --------------------- | ----------------- | ----------------------- | -------------------------- | --------------------------- |
| **imgcook**           | Rule-based + ML   | Y-axis overlap grouping | Row/column alignment check | IoU > 0.1 → absolute        |
| **FigmaToCode**       | Metadata-first    | Read Figma Auto Layout  | None (no inference)        | N/A                         |
| **Grida**             | Rules + ML        | Similar to imgcook      | Similar to imgcook         | Similar to imgcook          |
| **Phoenix Codie**     | Rule-based        | Position analysis       | N/A                        | Explicit overlap check      |
| **Allen's Algorithm** | Graph-based       | Interval relations      | Two-phase inference        | Contains/overlaps relations |
| **GRIDS**             | MILP optimization | N/A                     | Mathematical optimization  | Constraint-based            |
| **Screen Parsing**    | ML-based          | ML prediction           | ML prediction              | ML prediction               |

### Strengths and Weaknesses

| Approach           | Strengths                      | Weaknesses                         |
| ------------------ | ------------------------------ | ---------------------------------- |
| **Rule-based**     | Predictable, explainable, fast | Limited flexibility, manual tuning |
| **Metadata-first** | Accurate when metadata exists  | Fails without designer cooperation |
| **ML-based**       | Handles complex patterns       | Requires training data, black box  |
| **MILP**           | Mathematically optimal         | Computationally expensive          |
| **Hybrid**         | Best of both worlds            | Complex implementation             |

---

## Detection Criteria & Thresholds

### Universal Thresholds (Industry Consensus)

```typescript
const LAYOUT_THRESHOLDS = {
  // Axis overlap tolerances
  ROW_GROUPING_TOLERANCE: 2, // px - Y-axis overlap for same row
  COLUMN_GROUPING_TOLERANCE: 2, // px - X-axis overlap for same column

  // Gap analysis
  GAP_VARIANCE_THRESHOLD: 0.2, // 20% - Max coefficient of variation
  GAP_ROUNDING_GRID: 4, // px - Snap gaps to multiples

  // Alignment
  ALIGNMENT_TOLERANCE: 2, // px - Edge alignment detection

  // Size homogeneity
  SIZE_CV_THRESHOLD: 0.2, // 20% - Max size variance for grid

  // Overlap detection
  IOU_OVERLAP_THRESHOLD: 0.1, // 10% - Mark as overlapping
  IOU_SIGNIFICANT_OVERLAP: 0.5, // 50% - Definitely overlapping

  // Confidence thresholds
  MIN_FLOW_CONFIDENCE: 0.3, // Minimum to accept flow layout
  MIN_GRID_CONFIDENCE: 0.6, // Minimum to accept grid layout

  // Element counts
  MIN_GRID_ELEMENTS: 4, // Minimum for grid detection
  MIN_GRID_ROWS: 2, // Minimum rows for grid
  MIN_GRID_COLUMNS: 2, // Minimum columns for grid
};
```

### Gap Rounding Values

```typescript
// Common design system spacing values
const COMMON_GAP_VALUES = [0, 4, 8, 12, 16, 20, 24, 32, 40, 48, 64];

function roundGapToCommon(gap: number): number {
  return COMMON_GAP_VALUES.reduce((prev, curr) =>
    Math.abs(curr - gap) < Math.abs(prev - gap) ? curr : prev,
  );
}
```

---

## Overlap Detection

### IoU (Intersection over Union) Calculation

```typescript
interface Rect {
  x: number;
  y: number;
  width: number;
  height: number;
}

function calculateIoU(a: Rect, b: Rect): number {
  // Calculate intersection
  const xOverlap = Math.max(0, Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x));
  const yOverlap = Math.max(0, Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y));
  const intersection = xOverlap * yOverlap;

  // Calculate union
  const areaA = a.width * a.height;
  const areaB = b.width * b.height;
  const union = areaA + areaB - intersection;

  return union > 0 ? intersection / union : 0;
}
```

### Overlap Classification

```typescript
type OverlapType = "none" | "adjacent" | "partial" | "significant" | "contained";

function classifyOverlap(a: Rect, b: Rect): OverlapType {
  const iou = calculateIoU(a, b);

  if (iou === 0) {
    // Check if adjacent (touching but not overlapping)
    const gap = calculateGap(a, b);
    return gap <= 2 ? "adjacent" : "none";
  }

  if (iou < 0.1) return "partial";
  if (iou < 0.5) return "significant";
  return "contained";
}

function shouldUseAbsolutePosition(element: Rect, siblings: Rect[]): boolean {
  for (const sibling of siblings) {
    const overlap = classifyOverlap(element, sibling);
    if (overlap === "partial" || overlap === "significant" || overlap === "contained") {
      return true;
    }
  }
  return false;
}
```

### Z-Index Inference

```typescript
// When elements overlap, infer z-order from:
// 1. Figma layer order (later = on top)
// 2. Size (smaller elements usually on top)
// 3. Type (text/icons usually on top of backgrounds)

function inferZIndex(elements: Element[]): Map<Element, number> {
  const zIndexMap = new Map<Element, number>();

  // Sort by layer order (Figma provides this)
  const sorted = [...elements].sort((a, b) => a.layerOrder - b.layerOrder);

  sorted.forEach((el, index) => {
    zIndexMap.set(el, index);
  });

  return zIndexMap;
}
```

---

## Implementation Recommendations

### For This Project (Figma-Context-MCP)

Based on the research, here's the recommended algorithm:

#### 1. Pre-filtering: Homogeneity Check (IMPLEMENTED)

```typescript
// Already implemented in detector.ts
export function filterHomogeneousForGrid(
  rects: ElementRect[],
  nodeTypes?: string[],
): ElementRect[] {
  const homogeneity = analyzeHomogeneity(rects, nodeTypes, 0.2);
  return homogeneity.homogeneousElements;
}
```

#### 2. Overlap Detection (TO IMPLEMENT)

```typescript
function detectOverlappingElements(nodes: SimplifiedNode[]): {
  flowElements: SimplifiedNode[];
  stackedElements: SimplifiedNode[];
} {
  const rects = nodes.map(nodeToRect);
  const flowElements: SimplifiedNode[] = [];
  const stackedElements: SimplifiedNode[] = [];

  for (let i = 0; i < nodes.length; i++) {
    const hasOverlap = rects.some((other, j) => i !== j && calculateIoU(rects[i], other) > 0.1);

    if (hasOverlap) {
      stackedElements.push(nodes[i]);
    } else {
      flowElements.push(nodes[i]);
    }
  }

  return { flowElements, stackedElements };
}
```

#### 3. Child Element Style Cleanup (TO IMPLEMENT)

```typescript
function cleanChildStylesForFlexParent(child: SimplifiedNode): void {
  if (child.cssStyles) {
    // Remove position: absolute when parent is flex/grid
    if (child.cssStyles.position === "absolute") {
      delete child.cssStyles.position;
    }

    // Remove left/top (now handled by flex/grid)
    delete child.cssStyles.left;
    delete child.cssStyles.top;

    // Keep width/height for flex items (used by flex-basis)
  }
}

function cleanChildStylesForGridParent(child: SimplifiedNode): void {
  if (child.cssStyles) {
    delete child.cssStyles.position;
    delete child.cssStyles.left;
    delete child.cssStyles.top;

    // Width/height may be redundant if grid tracks define sizes
    // But keep them for explicit sizing
  }
}
```

#### 4. Default Value Removal (TO IMPLEMENT)

```typescript
const CSS_DEFAULT_VALUES: Record<string, string[]> = {
  fontWeight: ["400", "normal"],
  textAlign: ["left", "start"],
  flexDirection: ["row"],
  position: ["static"],
  opacity: ["1"],
  backgroundColor: ["transparent", "rgba(0,0,0,0)"],
  borderWidth: ["0", "0px"],
};

function removeDefaultValues(cssStyles: CSSStyle): CSSStyle {
  const cleaned: CSSStyle = {};

  for (const [key, value] of Object.entries(cssStyles)) {
    const defaults = CSS_DEFAULT_VALUES[key];
    if (defaults && defaults.includes(String(value))) {
      continue; // Skip default value
    }
    cleaned[key] = value;
  }

  return cleaned;
}
```

### Complete Detection Pipeline

```
┌─────────────────────────────────────────────────────────────────┐
│                    Layout Detection Pipeline                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│   1. INPUT: Array of child elements with absolute coordinates    │
│                            ↓                                      │
│   2. OVERLAP CHECK: Separate overlapping elements                │
│      - IoU > 0.1 → stackedElements (keep absolute)               │
│      - IoU ≤ 0.1 → flowElements (continue analysis)              │
│                            ↓                                      │
│   3. HOMOGENEITY CHECK: Filter similar elements                  │
│      - Size CV < 20% AND same types → homogeneous                │
│      - Mixed sizes/types → heterogeneous                         │
│                            ↓                                      │
│   4. ROW GROUPING: Y-axis overlap (2px tolerance)                │
│      - 1 row → horizontal flex                                   │
│      - Multiple rows → continue                                  │
│                            ↓                                      │
│   5. GRID CHECK (homogeneous only):                              │
│      - rows ≥ 2 AND columns ≥ 2                                  │
│      - Column alignment ≥ 80%                                    │
│      - Confidence ≥ 0.6                                          │
│      → YES: display: grid                                        │
│      → NO: continue                                              │
│                            ↓                                      │
│   6. FLEX DIRECTION:                                             │
│      - Dominant direction by element count                       │
│      - Row if most elements horizontal                           │
│      - Column if most elements vertical                          │
│                            ↓                                      │
│   7. CHILD CLEANUP:                                              │
│      - Remove position: absolute                                 │
│      - Remove left/top                                           │
│      - Remove default CSS values                                 │
│                            ↓                                      │
│   8. OUTPUT: LayoutInfo with clean child styles                  │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘
```

---

## References

### Academic Papers

1. **"A Layout Inference Algorithm for Graphical User Interfaces"** (2015)

   - [ScienceDirect](https://www.sciencedirect.com/science/article/abs/pii/S0950584915001718)
   - [ResearchGate PDF](https://www.researchgate.net/publication/283526120_A_layout_inference_algorithm_for_Graphical_User_Interfaces)
   - Key: Allen's Interval Algebra, 97% layout faithfulness

2. **"GRIDS: Interactive Layout Design with Integer Programming"** (CHI 2020)

   - [Project Page](https://userinterfaces.aalto.fi/grids/)
   - [GitHub](https://github.com/aalto-ui/GRIDS)
   - [Paper PDF](https://acris.aalto.fi/ws/portalfiles/portal/40720569/CHI2020_Dayama_GRIDS.pdf)
   - Authors: Niraj Dayama, Kashyap Todi, Taru Saarelainen, Antti Oulasvirta (Aalto University)

3. **"Screen Parsing: Towards Reverse Engineering of UI Models from Screenshots"** (UIST 2021)

   - [CMU ML Blog](https://blog.ml.cmu.edu/2021/12/10/understanding-user-interfaces-with-screen-parsing/)
   - [Paper PDF](https://www.cs.cmu.edu/~jbigham/pubs/pdfs/2021/screen-parsing.pdf)
   - [ACM Digital Library](https://dl.acm.org/doi/fullHtml/10.1145/3472749.3474763)
   - Authors: Jason Wu, Xiaoyi Zhang, Jeff Nichols, Jeffrey P. Bigham

4. **"UI Semantic Group Detection"** (arXiv 2024) - arXiv:2403.04984v1

5. **"UIHASH: Grid-Based UI Similarity"** - Jun Zeng et al.

### Open Source Projects

1. [FigmaToCode](https://github.com/bernaferrari/FigmaToCode) - Generate responsive pages on HTML, Tailwind, Flutter, SwiftUI
2. [GRIDS](https://github.com/aalto-ui/GRIDS) - MILP-based grid layout generation (Python + Gurobi)
3. [Grida](https://github.com/gridaco/grida) - Design-to-code platform
4. [Yoga](https://github.com/facebook/yoga) - Facebook's cross-platform Flexbox layout engine

### Industry Resources

1. **imgcook (Alibaba)**

   - [Layout Algorithm Blog](https://www.alibabacloud.com/blog/imgcook-3-0-series-layout-algorithm-design-based-code-generation_597856)
   - [How imgcook Works](https://medium.com/imgcook/imgcook-how-are-codes-generated-intelligently-from-design-files-in-alibaba-98ba8e55246d)
   - [100% Accuracy Rate](https://www.alibabacloud.com/blog/imgcook-intelligent-code-generation-from-design-drafts-with-a-100%25-accuracy-rate_598093)

2. **Locofy**

   - [Design Optimiser Docs](https://www.locofy.ai/docs/lightning/design-optimiser/)
   - [Auto Layout to Responsive Code](https://www.locofy.ai/docs/classic/design-structure/responsiveness/auto-layout/)

3. **Anima**

   - [Auto-Flexbox Introduction](https://www.animaapp.com/blog/design-to-code/introducing-auto-flexbox/)
   - [Flexbox from Constraints](https://www.animaapp.com/blog/product-updates/producing-flexbox-responsive-code-based-on-figma-adobe-xd-and-sketch-constraints/)

4. **CSS Standards**

   - [CSS Grid Layout Module Level 1](https://www.w3.org/TR/css-grid-1/) - W3C
   - [CSS Flexible Box Layout](https://www.w3.org/TR/css-flexbox-1/) - W3C
   - [Understanding Layout Algorithms](https://www.joshwcomeau.com/css/understanding-layout-algorithms/) - Josh W. Comeau

5. **Foundational**
   - [Allen's Interval Algebra](https://en.wikipedia.org/wiki/Allen's_interval_algebra) - Wikipedia
   - [Figma Grid Auto-Layout](https://help.figma.com/hc/en-us/articles/31289469907863) - Figma Help
   - [IoU Explained](https://www.v7labs.com/blog/intersection-over-union-guide) - V7 Labs

---

## Appendix: Code Examples

### A. Complete Flow Detection

```typescript
export function detectFlowLayout(elements: Element[]): LayoutInfo {
  // Step 1: Check for overlaps
  const { flowElements, stackedElements } = detectOverlappingElements(elements);

  if (flowElements.length < 2) {
    return { type: "absolute", elements: stackedElements };
  }

  // Step 2: Group into rows
  const rows = groupIntoRows(flowElements, THRESHOLDS.ROW_GROUPING_TOLERANCE);

  // Step 3: Determine direction
  if (rows.length === 1) {
    // Single row = horizontal flex
    const gapAnalysis = analyzeGaps(rows[0], "horizontal");
    return {
      type: "flex",
      direction: "row",
      gap: gapAnalysis.averageGap,
      alignment: detectAlignment(rows[0], "vertical"),
      justifyContent: detectJustifyContent(rows[0], "horizontal"),
    };
  }

  // Step 4: Check for grid (multiple rows)
  const gridResult = detectGridLayout(flowElements);
  if (gridResult.isGrid && gridResult.confidence >= 0.6) {
    return {
      type: "grid",
      columns: gridResult.columnCount,
      rows: gridResult.rowCount,
      columnGap: gridResult.columnGap,
      rowGap: gridResult.rowGap,
      trackWidths: gridResult.trackWidths,
    };
  }

  // Step 5: Fallback to column flex
  const rowGapAnalysis = analyzeGaps(
    rows.map((r) => r[0]),
    "vertical",
  );
  return {
    type: "flex",
    direction: "column",
    gap: rowGapAnalysis.averageGap,
    alignment: detectAlignment(rows.flat(), "horizontal"),
  };
}
```

### B. Complete Grid Detection

```typescript
export function detectGridLayout(elements: ElementRect[]): GridAnalysisResult {
  // Step 1: Group into rows
  const rows = groupIntoRows(elements, 2);

  if (rows.length < 2) {
    return { isGrid: false, confidence: 0 };
  }

  // Step 2: Check column count consistency
  const columnCounts = rows.map((r) => r.length);
  const countVariance = coefficientOfVariation(columnCounts);

  if (countVariance > 0.2) {
    return { isGrid: false, confidence: 0 };
  }

  // Step 3: Extract column positions
  const columnPositions = extractColumnPositions(rows);
  const alignmentResult = checkColumnAlignment(columnPositions, 4);

  // Step 4: Calculate gaps
  const columnGaps = calculateColumnGaps(rows);
  const rowGaps = calculateRowGaps(rows);

  // Step 5: Calculate confidence
  let confidence = 0;
  if (rows.length >= 2) confidence += 0.2;
  if (rows.length >= 3) confidence += 0.1;
  if (countVariance < 0.1) confidence += 0.2;
  if (alignmentResult.isAligned) confidence += 0.25;
  if (coefficientOfVariation(columnGaps) < 0.2) confidence += 0.1;
  if (coefficientOfVariation(rowGaps) < 0.2) confidence += 0.1;

  // Step 6: Calculate track sizes
  const trackWidths = calculateTrackWidths(rows, alignmentResult.alignedPositions);
  const trackHeights = rows.map((row) => Math.max(...row.map((el) => el.height)));

  return {
    isGrid: confidence >= 0.6,
    confidence,
    rowCount: rows.length,
    columnCount: Math.max(...columnCounts),
    rowGap: roundGapToCommon(average(rowGaps)),
    columnGap: roundGapToCommon(average(columnGaps)),
    trackWidths,
    trackHeights,
  };
}
```

```
Page 2/6FirstPrevNextLast