This is page 1 of 3. Use http://codebase.md/mhmzdev/figma-flutter-mcp?page={x} to view the full context.
# Directory Structure
```
├── .changeset
│   └── config.json
├── .github
│   └── workflows
│       └── release.yml
├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── docs
│   ├── cursor_rules_example.md
│   ├── figma-flutter-mcp.md
│   ├── figma-framework-mcp.md
│   ├── getting-started.md
│   └── images
│       ├── button.png
│       ├── figma-flutter-mcp.png
│       ├── screen.png
│       ├── svg.gif
│       ├── svgs_clean.gif
│       ├── text-style-frame.png
│       └── theme-frame.png
├── LICENSE.md
├── package.json
├── README.ja.md
├── README.ko.md
├── README.md
├── README.zh-cn.md
├── README.zh-tw.md
├── src
│   ├── cli.ts
│   ├── config.ts
│   ├── extractors
│   │   ├── colors
│   │   │   ├── core.ts
│   │   │   ├── extractor.ts
│   │   │   ├── index.ts
│   │   │   └── types.ts
│   │   ├── components
│   │   │   ├── core.ts
│   │   │   ├── deduplicated-extractor.ts
│   │   │   ├── extractor.ts
│   │   │   ├── index.ts
│   │   │   ├── types.ts
│   │   │   └── variant-analyzer.ts
│   │   ├── flutter
│   │   │   ├── global-vars.ts
│   │   │   ├── index.ts
│   │   │   ├── style-library.ts
│   │   │   └── style-merger.ts
│   │   ├── screens
│   │   │   ├── core.ts
│   │   │   ├── extractor.ts
│   │   │   ├── index.ts
│   │   │   └── types.ts
│   │   └── typography
│   │       ├── core.ts
│   │       ├── extractor.ts
│   │       ├── index.ts
│   │       └── types.ts
│   ├── figma-config.ts
│   ├── server.ts
│   ├── services
│   │   └── figma.ts
│   ├── tools
│   │   ├── flutter
│   │   │   ├── assets
│   │   │   │   ├── asset-manager.ts
│   │   │   │   ├── assets.ts
│   │   │   │   └── svg-assets.ts
│   │   │   ├── components
│   │   │   │   ├── component-tool.ts
│   │   │   │   ├── deduplicated-helpers.ts
│   │   │   │   └── helpers.ts
│   │   │   ├── index.ts
│   │   │   ├── screens
│   │   │   │   ├── helpers.ts
│   │   │   │   └── screen-tool.ts
│   │   │   ├── semantic-detection.ts
│   │   │   ├── theme
│   │   │   │   ├── colors
│   │   │   │   │   ├── theme-generator.ts
│   │   │   │   │   └── theme-tool.ts
│   │   │   │   └── typography
│   │   │   │       ├── typography-generator.ts
│   │   │   │       └── typography-tool.ts
│   │   │   └── visual-context.ts
│   │   └── index.ts
│   ├── types
│   │   ├── errors.ts
│   │   ├── figma.ts
│   │   └── flutter.ts
│   └── utils
│       ├── figma-url-parser.ts
│       ├── helpers.ts
│       ├── logger.ts
│       ├── retry.ts
│       └── validation.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
node_modules/
.env
package-lock.json
dist/
output/
reports/
.cursor/
prompts/
*.DS_STORE
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
<div align="center">
  <img src="docs/images/figma-flutter-mcp.png" alt="Theme Setup Example" style="max-width: 100%; height: auto;">
  <br>
  <h1>Figma to Flutter MCP Server</h1>
   <p>
    🌐 Available in:
    <a href="README.ko.md">한국어 (Korean)</a> |
    <a href="README.ja.md">日本語 (Japanese)</a> |
    <a href="README.zh-cn.md">简体中文 (Simplified Chinese)</a> |
    <a href="README.zh-tw.md">繁體中文 (Traditional Chinese)</a>
  </p>
  <h3>Utilize Figma's rich data in your coding agent.<br/>Implement designs in Flutter way!</h3>
  <a href="https://npmcharts.com/compare/figma-flutter-mcp?interval=30">
    <img alt="weekly downloads" src="https://img.shields.io/npm/dm/figma-flutter-mcp.svg">
  </a>
  <a href="https://github.com/mhmzdev/figma-flutter-mcp/blob/main/LICENSE">
    <img alt="MIT License" src="https://img.shields.io/github/license/mhmzdev/figma-flutter-mcp" />
  </a>
  <a href="https://twitter.com/mhmzdev">
    <img alt="Twitter" src="https://img.shields.io/twitter/url?url=https%3A%2F%2Fx.com%2Fmhmzdev&label=%40mhmzdev" />
  </a>
</div>
<br>
Use [Cursor](https://cursor.sh) or other AI-powered tools to access Figma's rich files, data, components and much more using [MCP server](https://modelcontextprotocol.io/).
## 📋 Table of Contents
- [🦋 Observable Flutter #70](#-observable-flutter-70)
- [🎥 Short Video Demo](#-short-video-demo)
- [📝 Getting Started](#-getting-started)
- [📚 How it works](#-how-it-works--details-here)
- [🛠️ Usage](#-usage)
  - [🔑 Figma API Key](#-figma-api-key)
  - [🏹 MCP in Cursor](#-mcp-in-cursor)
  - [🚀 Quick Start for Local Testing](#-quick-start-for-local-testing)
- [🧱 Basic Workflow](#-basic-workflow)
  - [🤖 AI Coding Agent Assistance](#-ai-coding-agent-assistance)
  - [⚠️ If SVG assets don’t work with screen generation](#-if-svg-assets-dont-work-with-screen-generation)
- [⚠️ Disclaimers](#-disclaimers)
- [🙌🏼 Acknowledgments](#-acknowledgments)
- [🧱 Other framworks](#-other-framworks)
- [🔑 License](#-license)
- [🙋♂️ Author](#-author)
  - [Muhammad Hamza](#muhammad-hamza)
## 🦋 Observable Flutter #70
Featured on Observable Flutter with enhanced explanation and demo:
<a href="https://www.youtube.com/live/d7qrvytOxSA?si=ESY8hPJpQm_OY4Ye">
  <img src="https://i.ytimg.com/vi/d7qrvytOxSA/hq720.jpg?sqp=-oaymwEnCNAFEJQDSFryq4qpAxkIARUAAIhCGAHYAQHiAQoIGBACGAY4AUAB&rs=AOn4CLAtjlrlbNDcV_MQ-_MHJN3KAgwpKw" alt="Observable Flutter Figma to Flutter MCP" style="max-width: 100%; height: 300px;">
</a>
## 🎥 Short Video Demo
Showcased almost all the features of Figma Flutter MCP with real figma design.
- English: https://youtu.be/lJlfOfpl2sI
- Urdu/Hindi: https://youtu.be/mepPWpIZ61M
## 📝 [Getting Started](docs/getting-started.md)
You may explore the detailed [getting started](docs/getting-started.md) docs or the [demo video](https://youtu.be/lJlfOfpl2sI) as quick-start. As its a First Release hence there's a lot of room for improvements so you can checkout the [issues](https://github.com/mhmzdev/figma-flutter-mcp/issues) to see what else there's to work or to improve.
## 📚 How it works | [Details Here](docs/figma-flutter-mcp.md)
1. [Components/Widgets](src/extractors/components/)
- ✅ Extract Figma node data: Layout, styling, dimensions, colors, text content, etc.
- ✅ Analyze structure: Child elements, nested components, visual importance
- ✅ Provide guidance: Suggest Flutter widgets and implementation patterns
- ❌ NOT generating actual Flutter code files
2. [Screens](src/extractors/screens/)
- ✅ Extract screen metadata: Device type, orientation, dimensions
- ✅ Identify sections: Header, footer, navigation, content areas
- ✅ Analyze navigation: Tab bars, app bars, drawers, navigation elements
- ✅ Provide Scaffold guidance: Suggest Flutter screen structure
- ❌ NOT generating actual Flutter screen
Since its just helping AI write Flutter code so it means the better your prompt will be the better results you'll get.
## 🛠️ Usage
Following steps shows a minimal usage and setup instructions:
### 🔑 Figma API Key
You will need to create a Figma access token to use this server. Instructions on how to create a Figma API access token can be found [here](https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens).
### 🏹 MCP in Cursor
Once you've the FIGMA API KEY, you can setup the MCP in cursor as follows:
1. Press CMD + Shift + P (Ctrl on Windows)
2. Type "Open MCP Settings"
3. Click on "Add new MCP"
4. Paste the below json object
#### MacOS/Linux
```
{
  "mcpServers": {
    "Figma Flutter MCP": {
      "command": "npx",
      "args": ["-y", "figma-flutter-mcp", "--figma-api-key=YOUR-API-KEY", "--stdio"]
    }
  }
}
```
#### Windows
```
{
  "mcpServers": {
    "Figma Flutter MCP": {
      "command": "cmd",
      "args": ["/c", "npx", "-y", "figma-flutter-mcp", "--figma-api-key=YOUR-API-KEY", "--stdio"]
    }
  }
}
```
> NOTE: If you've installed this MCP as `npm` package make sure to keep it updated to latest version. Sometimes, it caches the old version and keep showing you error like "Not being able to use tool call" or "Figma API key setup is not working" etc.
### 🚀 Quick Start for Local Testing
#### Prerequisites
- Node.js 18+
- Figma API Key (Access Token)
- Cursor AI IDE with MCP support
- Flutter SDK
For quick local testing, you can run the server via HTTP instead of stdio:
```bash
# Clone and setup
git clone <your-repo-url> figma-flutter-mcp
cd figma-flutter-mcp
npm install
# Create .env file with your Figma API key
echo "FIGMA_API_KEY=your-figma-api-key-here" > .env
# Start HTTP server for local testing
npm run dev
```
Then add this to your MCP client configuration:
```json
{
  "mcpServers": {
    "local-figma-flutter": {
      "url": "http://localhost:3333/mcp"
    }
  }
}
```
See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed instructions.
## 🧱 Basic Workflow
### 🤖 AI Coding Agent Assistance
For better results you can setup some instructions in following files as per your AI Coding Agent:
- Cursor: `.cursor/rules/fluttering.mdc`
- Claude: `CLAUDE.md`
- Gemini CLI: `GEMINI.md`
This way your AI agent will use the MCP's output and ensure the flutter code is as per your project requirements and structure. You can checkout an example of [cursor rules](docs/cursor_rules_example.md) that I used for testing this out.
1. **Setup Theme & Typography**: The most efficient way, put two frames in Figma with Theme colors and Typography samples on it. For instance:


- Figma Desktop: Select the frame and press CMD + L or Ctrl + L
- Figma Web: Select the frame and copy the URL
> 💡 HINT: The valid URL will contain a FILE ID and NODE ID params
```
"Setup flutter theme from <figma_link> including Colors and Typography.
```
2. **Widget Generation**: The most efficient way, use COMPONENTS in figma. For example:

This one has 2 variants with enabled and disabled states.
```
"Create this widget in flutter from figma COMPONENT link: <figma_link>, use named constructors for variants and break the files in smaller parts for code readability."
```
If you **do not** have COMPONENTS in figma, you can use FRAME just prompt the AI that you want this to be a widget and it will handle the rest.
3. **Full Screen Generation**: If there are any IMAGE ASSETS (.png, .jpeg, .jpg etc.) available, it will export them and put them in `assets/` along with `pubspec.yaml`
<img src="docs/images/screen.png" alt="Screen" height="500" width="auto">
```
"Design this intro screen from the figma link <figma_link>, ensure the code is readable by having smaller files."
```
4. **Assets Export**:
- Image Assets: Will work automatically when generating screens
```
"Export this image asset from figma link: <figma_link>
```
- SVG Assets: Will NOT work automatically if they are scrambled or are ungrouped, explained below.
```
"Export this as an SVG asset from Figma link: <figma_link>"
```
#### ⚠️ If SVG assets don’t work with screen generation
* In Figma vectors include icons and pen-tool shapes, so bulk exports may grab unintended nodes;
  *  Recommend exporting SVGs **separately** i.e. to take them out an an independent FRAME or GROUP
  *  Here's how the separation of SVGs looks like:
<img src="docs/images/svgs_clean.gif" alt="Screen" height="500" width="auto">
<br>
* Here's an example of identifying a GOOD vs BAD svg while exporting them:
<br>
<img src="docs/images/svg.gif" alt="Screen" height="500" width="auto">
## ⚠️ Disclaimers
- **Use Case**: At this stage, its highly recommend to NOT use it to develop scalable apps rather try and play it with MVPs, smaller and explanatory tasks.
- **Figma Design**: Since we're using Figma's API to fetch the node and its details, so the better design you have the more better it will interpret for the AI to consume i.e. auto layouts, frame usage over group usage, consistently aligned across the board.
- **Rate limiting**: Heavy usage may trigger Figma rate limits (e.g., HTTP 429). The server includes retry with backoff, but it does not bypass Figma limits. If you encounter rate limits, wait a few minutes and reduce the request volume.
## 🙌🏼 Acknowledgments
I came across [Figma Context MCP](https://github.com/GLips/Figma-Context-MCP) by [Graham Lipsman](https://x.com/glipsman) that sparks this motivation for me to develop Figma to Flutter explicitly having features like:
- Assets exports
- Colors and Theme setups
- Widget tree and full screen building
Others coming soon...
## 🧱 Other framworks
If you want to develop this for React, Angular, React Native, Vue or any other framework. I've added a detailed doc [Figma Framework MCP](docs/figma-framework-mcp.md) that you can explore and get started. Meanwhile I'll maintain a list here if someone's already doing this for framework specific Figma's MCP servers.
- ...
- ...
## 🔑 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE.md) file for details
## 🙋♂️ Author
#### Muhammad Hamza
[](https://www.linkedin.com/in/mhmzdev)
You can also follow my GitHub Profile to stay updated about my latest projects:
[](https://github.com/mhmzdev)
If you liked the repo then kindly support it by giving it a star ⭐!
Copyright (c) 2025 MUHAMMAD HAMZA
---
**Built with ❤️ for designers and developers who want to bridge the gap between design and code.**
```
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
```markdown
MIT License
Copyright (c) 2025 Muhammad Hamza (https://mhmz.dev)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
```markdown
# Contributing to Figma Flutter MCP
Thank you for your interest in contributing to Figma Flutter MCP! This guide will help you get started with development and testing.
## 🚀 Quick Start for Contributors
### Prerequisites
- Node.js 18+
- npm or yarn
- Figma API Key (for testing)
- Git
### Setup Development Environment
1. **Fork and Clone**
   ```bash
   git clone https://github.com/your-username/figma-flutter-mcp.git
   cd figma-flutter-mcp
   npm install
   ```
2. **Create .env file**
   ```bash
   # Create .env file with your Figma API key
   echo "FIGMA_API_KEY=your-figma-api-key-here" > .env
   ```
3. **Start Development Server**
   ```bash
   npm run dev
   ```
## 📋 Development Guidelines
### Code Style
- Use TypeScript for all new code
- Follow existing code patterns and conventions
- Use meaningful variable and function names
- Add docs in `docs/` if you think its neccessary
### Project Structure
```
src/
├── cli.mts              # CLI entry point
├── server.mts           # MCP server implementation
├── config.mts           # Configuration handling
├── extractors/          # Figma data extractors
│   ├── colors/
│   ├── components/
│   ├── screens/
│   └── typography/
├── tools/               # Flutter code generators
├── services/            # External service integrations
├── types/               # TypeScript type definitions
└── utils/               # Utility functions
```
### Making Changes
1. **Create a Branch**
   ```bash
   git checkout -b feature/your-feature-name
   # or
   git checkout -b fix/issue-description
   ```
2. **Make Your Changes**
   - Write clean, documented code
   - Add tests for new features
   - Update documentation as needed
3. **Test Your Changes**
   ```bash
   # Build and check for errors
   npm run build
   
   # Test locally
   npm run dev
   ```
4. **Commit and Push**
   ```bash
   git add .
   git commit -m "feat: add new feature description"
   git push origin feature/your-feature-name
   ```
5. **Create Pull Request**
   - Use descriptive titles and descriptions
   - Reference any related issues
   - Include screenshots/examples if applicable
## 🧪 Local Testing & Development
The project supports HTTP server mode for easier development and testing. This allows you to test MCP tools without setting up a full MCP client.
### Setting Up Your Environment
If you haven't already set up your Figma API key:
```bash
# Create .env file
echo "FIGMA_API_KEY=your-figma-api-key-here" > .env
```
**Get your Figma API Key:**
1. Go to [Figma Settings > Personal Access Tokens](https://www.figma.com/developers/api#access-tokens)
2. Generate a new personal access token
3. Copy the token and add it to your `.env` file
⚠️ **Important**: Never commit your `.env` file to version control. It's already included in `.gitignore`.
### Development Server Options
#### Using npm scripts (recommended)
```bash
# Start HTTP server on default port 3333
npm run dev
# Start HTTP server on a specific port
npm run dev:port 4000
# Start in stdio mode (for MCP clients)
npm run dev:stdio
```
#### Using direct commands
```bash
# Start HTTP server
npx tsx src/cli.mts --http
# Start HTTP server on specific port
npx tsx src/cli.mts --http --port 4000
# Start in stdio mode
npx tsx src/cli.mts --stdio
```
#### Using built version
```bash
# Build first
npm run build
# Start HTTP server
node dist/cli.mjs --http
# Start HTTP server on specific port
node dist/cli.mjs --http --port 4000
```
## Connecting to the Server
### MCP Client Configuration
To connect an MCP client to the local HTTP server, add this configuration to your MCP JSON config file:
```json
{
  "mcpServers": {
    "local-figma-flutter-mcp": {
      "url": "http://localhost:3333/mcp"
    }
  }
}
```
## Available Endpoints
When the HTTP server is running, the following endpoints are available:
- **POST /mcp** - Main Streamable HTTP endpoint for MCP communication
- **GET /mcp** - Session management for StreamableHTTP
- **DELETE /mcp** - Session termination for StreamableHTTP  
- **GET /sse** - Server-Sent Events endpoint (alternative transport)
- **POST /messages** - Message endpoint for SSE transport
## Environment Variables
You can configure the server using environment variables. The recommended approach is to use a `.env` file:
### Using .env file (Recommended)
```env
# Required: Your Figma API key
FIGMA_API_KEY=your-figma-api-key-here
# Optional: Enable HTTP mode by default
HTTP_MODE=true
# Optional: Set default HTTP port
HTTP_PORT=3333
```
## 📋 Pull Request Checklist
Before submitting a PR:
- [ ] Code builds without errors (`npm run build`)
- [ ] Tests pass (if applicable)
- [ ] Documentation updated
- [ ] PR description explains changes
- [ ] Related issues referenced
- [ ] Follows existing code style
Thank you for contributing to Figma Flutter MCP! 🚀
```
--------------------------------------------------------------------------------
/src/utils/helpers.ts:
--------------------------------------------------------------------------------
```typescript
```
--------------------------------------------------------------------------------
/src/types/flutter.ts:
--------------------------------------------------------------------------------
```typescript
// flutter related types
```
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
```json
{
  "$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
  "changelog": [
    "@changesets/changelog-github",
    {
      "repo": "mhmzdev/figma-flutter-mcp"
    }
  ],
  "commit": true,
  "fixed": [],
  "linked": [],
  "access": "public",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "ignore": []
}
```
--------------------------------------------------------------------------------
/src/extractors/flutter/index.ts:
--------------------------------------------------------------------------------
```typescript
// src/extractors/flutter/index.mts
export { 
  FlutterStyleLibrary, 
  FlutterCodeGenerator,
  type FlutterStyleDefinition,
  type StyleRelationship,
  type OptimizationReport
} from './style-library.js';
export {
  GlobalStyleManager,
  type GlobalVars
} from './global-vars.js';
export {
  StyleMerger,
  type MergeCandidate
} from './style-merger.js';
```
--------------------------------------------------------------------------------
/src/extractors/colors/index.ts:
--------------------------------------------------------------------------------
```typescript
// src/extractors/colors/index.mts
// Core functionality
export { ColorExtractor, extractThemeColors } from './core.js';
// Extractor functions
export {
    extractColorsFromThemeFrame,
    isTypographyNode
} from './extractor.js';
// Types
export type {
    ThemeColor,
    ColorDefinition,
    ColorExtractionContext,
    ColorExtractorFn,
    ThemeGenerationOptions
} from './types.js';
```
--------------------------------------------------------------------------------
/src/utils/logger.ts:
--------------------------------------------------------------------------------
```typescript
export class Logger {
    static log(...args: any[]): void {
        console.error('[MCP Server]', ...args);
    }
    static error(...args: any[]): void {
        console.error('[MCP Server ERROR]', ...args);
    }
    static warn(...args: any[]): void {
        console.error('[MCP Server WARN]', ...args);
    }
    static info(...args: any[]): void {
        console.error('[MCP Server INFO]', ...args);
    }
}
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "node",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "allowSyntheticDefaultImports": true
  },
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules",
    "dist"
  ]
}
```
--------------------------------------------------------------------------------
/src/extractors/typography/index.ts:
--------------------------------------------------------------------------------
```typescript
// src/extractors/typography/index.mts
// Core functionality
export { TypographyExtractor, extractThemeTypography } from './core.js';
// Extractor functions
export {
    extractTypographyFromThemeFrame,
    isTypographyDemoNode
} from './extractor.js';
// Types
export type {
    TypographyStyle,
    TypographyDefinition,
    TypographyExtractionContext,
    TypographyExtractorFn,
    TypographyExtractionOptions,
    TypographyGenerationOptions,
    FontWeightMapping,
    TextStyleHash
} from './types.js';
```
--------------------------------------------------------------------------------
/src/tools/flutter/index.ts:
--------------------------------------------------------------------------------
```typescript
// tools/flutter/index.mts
import type {McpServer} from "@modelcontextprotocol/sdk/server/mcp.js";
import {registerFlutterAssetTools} from "./assets/assets.js";
import {registerSvgAssetTools} from "./assets/svg-assets.js";
import {registerComponentTools} from "./components/component-tool.js";
import {registerScreenTools} from "./screens/screen-tool.js";
export function registerFlutterTools(server: McpServer, figmaApiKey: string) {
    // Register Flutter asset management tools
    registerFlutterAssetTools(server, figmaApiKey);
    // Register SVG asset management tools
    registerSvgAssetTools(server, figmaApiKey);
    // Register component analysis tools
    registerComponentTools(server, figmaApiKey);
    // Register screen analysis tools
    registerScreenTools(server, figmaApiKey);
}
```
--------------------------------------------------------------------------------
/src/extractors/screens/index.ts:
--------------------------------------------------------------------------------
```typescript
// src/extractors/screens/index.mts
// Core functionality
export {
    ScreenExtractor,
    analyzeScreen
} from './core.js';
export {
    extractScreenMetadata,
    extractScreenLayoutInfo,
    analyzeScreenSections,
    extractNavigationInfo,
    extractScreenAssets
} from './extractor.js';
// Types
export type {
    ScreenAnalysis,
    ScreenMetadata,
    ScreenLayoutInfo,
    ScreenSection,
    NavigationInfo,
    NavigationElement,
    ScreenAssetInfo,
    SkippedNodeInfo,
    ScreenExtractionOptions
} from './types.js';
// Convenience functions
export {
    parseComponentInput,
    isValidNodeIdFormat,
    extractIds,
    generateFigmaUrl,
    isFigmaUrl
} from '../../utils/figma-url-parser.js';
// Re-export commonly used types from figma types
export type {FigmaNode, FigmaColor, FigmaEffect} from '../../types/figma.js';
```
--------------------------------------------------------------------------------
/src/extractors/colors/types.ts:
--------------------------------------------------------------------------------
```typescript
// src/extractors/colors/types.mts
import type {FigmaNode} from '../../types/figma.js';
/**
 * Color definition from theme extraction
 */
export interface ThemeColor {
    name: string;
    hex: string;
    nodeId: string;
}
/**
 * Deduplicated color definition for design system
 */
export interface ColorDefinition {
    id: string;
    name: string;
    value: string; // hex color
    usage: 'primary' | 'secondary' | 'background' | 'text' | 'accent' | 'other';
    usageCount: number;
}
/**
 * Color extraction context
 */
export interface ColorExtractionContext {
    colorMap: Map<string, string>; // color value -> color ID
    currentDepth: number;
    maxDepth: number;
}
/**
 * Color extractor function type
 */
export type ColorExtractorFn = (
    node: FigmaNode,
    context: ColorExtractionContext,
    colorLibrary: ColorDefinition[]
) => string[] | null; // Returns color IDs
/**
 * Flutter theme generation options
 */
export interface ThemeGenerationOptions {
    generateThemeData?: boolean;
    includeColorScheme?: boolean;
    includeMaterialColors?: boolean;
}
```
--------------------------------------------------------------------------------
/src/tools/index.ts:
--------------------------------------------------------------------------------
```typescript
// src/tools/index.mts
import type {McpServer} from "@modelcontextprotocol/sdk/server/mcp.js";
import {registerFlutterTools} from "./flutter/index.js";
import {registerThemeTools} from "./flutter/theme/colors/theme-tool.js";
import {registerTypographyTools} from "./flutter/theme/typography/typography-tool.js";
export function registerAllTools(server: McpServer, figmaApiKey: string) {
    console.error('🛠️ Tools Debug - Starting tool registration...');
    // Register all tool categories
    registerFlutterTools(server, figmaApiKey);
    console.error('🛠️ Tools Debug - Flutter tools registered');
    registerThemeTools(server, figmaApiKey);
    console.error('🛠️ Tools Debug - Theme tools registered');
    registerTypographyTools(server, figmaApiKey);
    console.error('🛠️ Tools Debug - Typography tools registered');
    console.log("📋 Registered tool categories:");
    console.log("  🚀 Flutter tools - Widgets, Screens");
    console.log("  🏞️ Export assets - Images, SVGs");
    console.log("  🎨 Theme tools - Colors, Typography");
    console.log("  📝 Typography tools - Fonts, Sizes");
    console.error('🛠️ Tools Debug - All tools registration complete');
}
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
  "name": "figma-flutter-mcp",
  "version": "0.3.3",
  "description": "MCP server for Figma to Flutter conversion",
  "type": "module",
  "main": "dist/cli.js",
  "bin": {
    "figma-flutter-mcp": "dist/cli.js"
  },
  "files": [
    "dist/**/*",
    "README.md",
    "LICENSE.md"
  ],
  "scripts": {
    "build": "tsc",
    "start": "node dist/cli.js",
    "dev": "tsx src/cli.ts --http",
    "dev:stdio": "tsx src/cli.ts --stdio",
    "dev:port": "tsx src/cli.ts --http --port",
    "dev:remote": "tsx src/cli.ts --remote --port 3333",
    "changeset": "changeset add",
    "version": "changeset version && npm install --lockfile-only",
    "release": "changeset publish"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.0",
    "cors": "^2.8.5",
    "dotenv": "^17.2.1",
    "express": "^4.18.2",
    "node-fetch": "^3.3.2",
    "yargs": "^17.7.2",
    "zod": "^3.22.4"
  },
  "devDependencies": {
    "@changesets/changelog-github": "^0.5.1",
    "@changesets/cli": "^2.29.6",
    "@types/cors": "^2.8.17",
    "@types/express": "^4.17.21",
    "@types/node": "^20.10.0",
    "@types/node-fetch": "^2.6.9",
    "@types/yargs": "^17.0.32",
    "tsx": "^4.6.0",
    "typescript": "^5.3.0"
  },
  "keywords": [
    "figma",
    "flutter",
    "mcp",
    "claude",
    "design"
  ],
  "repository": "github:mhmzdev/figma-flutter-mcp",
  "homepage": "https://github.com/mhmzdev/figma-flutter-mcp#readme",
  "engines": {
    "node": ">=18.0.0"
  }
}
```
--------------------------------------------------------------------------------
/src/extractors/components/index.ts:
--------------------------------------------------------------------------------
```typescript
// src/extractors/components/index.mts
// Core functionality
export {
    ComponentExtractor,
    analyzeComponent,
    analyzeComponentWithVariants
} from './core.js';
export {
    extractMetadata,
    extractLayoutInfo,
    extractStylingInfo,
    analyzeChildren,
    createNestedComponentInfo,
    createComponentChild,
    calculateVisualImportance,
    isComponentNode,
    determineLayoutType,
    hasPadding,
    extractPadding,
    convertFillToColorInfo,
    convertStrokeInfo,
    categorizeEffects,
    extractCornerRadius,
    extractBasicLayout,
    extractBasicStyling,
    extractTextInfo,
    rgbaToHex
} from './extractor.js';
export {VariantAnalyzer} from './variant-analyzer.js';
// Deduplicated extractor
export {
    DeduplicatedComponentExtractor,
    type DeduplicatedComponentAnalysis,
    type DeduplicatedComponentChild
} from './deduplicated-extractor.js';
// Types
export type {
    ComponentAnalysis,
    ComponentMetadata,
    LayoutInfo,
    StylingInfo,
    ComponentChild,
    NestedComponentInfo,
    ComponentVariant,
    SkippedNodeInfo,
    CategorizedEffects,
    DropShadowEffect,
    InnerShadowEffect,
    BlurEffect,
    ColorInfo,
    StrokeInfo,
    CornerRadii,
    PaddingInfo,
    TextInfo,
    ComponentExtractionOptions
} from './types.js';
// Convenience functions
export {
    parseComponentInput,
    isValidNodeIdFormat,
    extractIds,
    generateFigmaUrl,
    isFigmaUrl
} from '../../utils/figma-url-parser.js';
// Re-export commonly used types from figma types
export type {FigmaNode, FigmaColor, FigmaEffect} from '../../types/figma.js';
```
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
```yaml
name: MCP Server Release
on:
  push:
    branches:
      - main
jobs:
  release:
    name: Create a PR for release workflow
    runs-on: ubuntu-latest
    # Skip if commit message contains [skip ci], [ci skip], or docs:
    if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]') && !startsWith(github.event.head_commit.message, 'docs:')"
    
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
      
      - name: Install dependencies
        run: npm install
      
      - name: Build the package
        run: npm run build
      
      - name: Create Version PR or Publish to NPM
        id: changesets
        uses: changesets/action@v1
        with:
          commit: "chore(release): version packages"
          title: "chore(release): version packages"
          version: npm run version
          publish: npm run release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
      - name: Create GitHub Release
        if: steps.changesets.outputs.published == 'true'
        uses: softprops/action-gh-release@v1
        with:
          tag_name: v${{ fromJson(steps.changesets.outputs.publishedPackages)[0].version }}
          name: Release v${{ fromJson(steps.changesets.outputs.publishedPackages)[0].version }}
          body: |
            ## What's Changed
            
            ${{ fromJson(steps.changesets.outputs.publishedPackages)[0].changelog }}
            
            **Full Changelog**: https://github.com/mhmzdev/figma-flutter-mcp/compare/v${{ fromJson(steps.changesets.outputs.publishedPackages)[0].version }}...HEAD
          draft: false
          prerelease: false
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
--------------------------------------------------------------------------------
/src/cli.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import {getServerConfig} from './config.js';
import {startMcpServer, startHttpServer} from './server.js';
async function startServer(): Promise<void> {
    const config = getServerConfig();
    if (config.isStdioMode) {
        await startMcpServer(config.figmaApiKey!);
    } else if (config.isHttpMode) {
        if (config.isRemoteMode) {
            console.log('Starting Figma Flutter MCP Server in REMOTE mode...');
            if (config.figmaApiKey) {
                console.log('✅ Server has fallback API key, but users can provide their own via:');
            } else {
                console.log('⚠️  Users MUST provide their own Figma API keys via:');
            }
            console.log('  - Authorization header (Bearer token)');
            console.log('  - X-Figma-Api-Key header');
            console.log('  - figmaApiKey query parameter');
            console.log('📝 Get API key: https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens');
        } else {
            console.log('Starting Figma Flutter MCP Server in HTTP mode...');
        }
        await startHttpServer(config.httpPort, config.figmaApiKey);
    } else {
        console.log('Starting Figma Flutter MCP Server...');
        console.log('⚠️  You must provide your Figma API key via:');
        console.log('   • CLI argument: --figma-api-key=YOUR_KEY');
        console.log('   • Environment: FIGMA_API_KEY=YOUR_KEY in .env file');
        console.log('');
        console.log('Available modes:');
        console.log('  --stdio   MCP client communication (requires API key)');
        console.log('  --http    Local testing via HTTP (requires API key)');
        console.log('  --remote  Remote deployment (users provide keys via HTTP headers)');
        console.log('');
        console.log('📝 Get your API key: https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens');
        console.log('Use --help for more options');
    }
}
startServer().catch((error) => {
    console.error("Failed to start server:", error);
    process.exit(1);
});
```
--------------------------------------------------------------------------------
/src/types/errors.ts:
--------------------------------------------------------------------------------
```typescript
// types/errors.mts
import type {Response} from 'node-fetch';
export class FigmaError extends Error {
    constructor(message: string, public code?: string, public statusCode?: number) {
        super(message);
        this.name = 'FigmaError';
    }
}
export class FigmaAuthError extends FigmaError {
    constructor(message: string = 'Invalid Figma access token') {
        super(message, 'AUTH_ERROR', 401);
        this.name = 'FigmaAuthError';
    }
}
export class FigmaNotFoundError extends FigmaError {
    constructor(resource: string, id: string) {
        super(`${resource} not found: ${id}`, 'NOT_FOUND', 404);
        this.name = 'FigmaNotFoundError';
    }
}
export class FigmaRateLimitError extends FigmaError {
    constructor(retryAfter?: number) {
        super(`Rate limit exceeded${retryAfter ? `. Retry after ${retryAfter} seconds` : ''}`, 'RATE_LIMIT', 429);
        this.name = 'FigmaRateLimitError';
        this.retryAfter = retryAfter;
    }
    public retryAfter?: number;
}
export class FigmaNetworkError extends FigmaError {
    constructor(message: string, public originalError?: Error) {
        super(`Network error: ${message}`, 'NETWORK_ERROR');
        this.name = 'FigmaNetworkError';
    }
}
export class FigmaParseError extends FigmaError {
    constructor(message: string, public rawResponse?: any) {
        super(`Failed to parse Figma response: ${message}`, 'PARSE_ERROR');
        this.name = 'FigmaParseError';
    }
}
export function createFigmaError(response: Response, message?: string): FigmaError {
    const defaultMessage = message || `Figma API error: ${response.status} ${response.statusText}`;
    switch (response.status) {
        case 401:
        case 403:
            return new FigmaAuthError(defaultMessage);
        case 404:
            return new FigmaNotFoundError('Resource', 'unknown');
        case 429:
            const retryAfter = response.headers.get('Retry-After');
            return new FigmaRateLimitError(retryAfter ? parseInt(retryAfter, 10) : undefined);
        default:
            return new FigmaError(defaultMessage, 'API_ERROR', response.status);
    }
}
```
--------------------------------------------------------------------------------
/src/extractors/screens/core.ts:
--------------------------------------------------------------------------------
```typescript
// src/extractors/screens/core.mts
import type {FigmaNode} from '../../types/figma.js';
import type {
    ScreenAnalysis,
    ScreenExtractionOptions
} from './types.js';
import {
    extractScreenMetadata,
    extractScreenLayoutInfo,
    analyzeScreenSections,
    extractNavigationInfo,
    extractScreenAssets
} from './extractor.js';
/**
 * Screen extraction and analysis class
 */
export class ScreenExtractor {
    private options: Required<ScreenExtractionOptions>;
    constructor(options: ScreenExtractionOptions = {}) {
        this.options = {
            maxSections: options.maxSections ?? 15,
            maxDepth: options.maxDepth ?? 4,
            includeHiddenNodes: options.includeHiddenNodes ?? false,
            extractNavigation: options.extractNavigation ?? true,
            extractAssets: options.extractAssets ?? true,
            deviceTypeDetection: options.deviceTypeDetection ?? true
        };
    }
    /**
     * Main screen analysis method
     */
    async analyzeScreen(node: FigmaNode): Promise<ScreenAnalysis> {
        const metadata = extractScreenMetadata(node);
        const layout = extractScreenLayoutInfo(node);
        
        const {sections, components, skippedNodes} = analyzeScreenSections(node, this.options);
        
        const navigation = this.options.extractNavigation 
            ? extractNavigationInfo(node)
            : { navigationElements: [] };
            
        const assets = this.options.extractAssets 
            ? extractScreenAssets(node)
            : [];
        return {
            metadata,
            layout,
            sections,
            components,
            navigation,
            assets,
            skippedNodes: skippedNodes.length > 0 ? skippedNodes : undefined
        };
    }
    /**
     * Get extractor options
     */
    getOptions(): Required<ScreenExtractionOptions> {
        return this.options;
    }
}
/**
 * Convenience function to analyze a screen
 */
export async function analyzeScreen(
    node: FigmaNode,
    options: ScreenExtractionOptions = {}
): Promise<ScreenAnalysis> {
    const extractor = new ScreenExtractor(options);
    return extractor.analyzeScreen(node);
}
```
--------------------------------------------------------------------------------
/src/extractors/typography/types.ts:
--------------------------------------------------------------------------------
```typescript
// src/extractors/typography/types.mts
import type {FigmaNode} from '../../types/figma.js';
/**
 * Typography style definition from theme extraction
 */
export interface TypographyStyle {
    name: string;
    fontFamily: string;
    fontSize: number;
    fontWeight: number;
    lineHeight: number;
    letterSpacing: number;
    nodeId: string;
    // Optional properties for enhanced text styles
    textAlign?: string;
    textDecoration?: string;
}
/**
 * Deduplicated typography definition for design system
 */
export interface TypographyDefinition {
    id: string;
    name: string;
    fontFamily: string;
    fontSize: number;
    fontWeight: number;
    lineHeight: number;
    letterSpacing: number;
    usage: 'heading' | 'body' | 'caption' | 'button' | 'label' | 'other';
    usageCount: number;
    // Additional Flutter-specific properties
    dartName?: string; // Dart-safe property name
}
/**
 * Typography extraction context
 */
export interface TypographyExtractionContext {
    typographyMap: Map<string, string>; // style hash -> typography ID
    currentDepth: number;
    maxDepth: number;
}
/**
 * Typography extractor function type
 */
export type TypographyExtractorFn = (
    node: FigmaNode,
    context: TypographyExtractionContext,
    typographyLibrary: TypographyDefinition[]
) => string[] | null; // Returns typography IDs
/**
 * Typography extraction options
 */
export interface TypographyExtractionOptions {
    maxDepth?: number;
    includeHiddenText?: boolean;
    minUsageCount?: number;
    excludeEmptyText?: boolean;
}
/**
 * Flutter typography generation options
 */
export interface TypographyGenerationOptions {
    generateAppText?: boolean;
    generateTextTheme?: boolean;
    familyVariableName?: string; // Name for shared font family variable
    includeLineHeight?: boolean;
    includeLetterSpacing?: boolean;
}
/**
 * Font weight mapping for Flutter
 */
export interface FontWeightMapping {
    [key: number]: string; // Figma weight -> Flutter FontWeight
}
/**
 * Text style hash components for deduplication
 */
export interface TextStyleHash {
    fontFamily: string;
    fontSize: number;
    fontWeight: number;
    lineHeight: number;
    letterSpacing: number;
}
```
--------------------------------------------------------------------------------
/docs/cursor_rules_example.md:
--------------------------------------------------------------------------------
```markdown
# Flutter & Dart LLM Assistant Prompt
You are a **senior Flutter & Dart developer** experienced in building **production-ready apps**. You follow best practices, performance optimization techniques, and clean, maintainable architecture.
## Responsibilities
1. Help me **write, refactor, or debug** Dart and Flutter code.
2. Explain code and concepts **clearly and concisely**.
3. Follow **best practices**: null safety, widget structure, idiomatic Dart, and clean state management.
4. Prefer **private widget classes over function widgets**.
5. Always give **code examples** when needed, and provide **short, meaningful explanations**.
6. For any feature you write that involves business logic or architecture, create a Markdown documentation file in `docs/` explaining the purpose. Update this file if you make changes in any of the related feature and search docs before applying feature to use the existing ones as your context
7. Use `///` comments to document each function, focusing on the **“why”**, not the “how”.
8. When unsure, **ask clarifying questions before coding**.
## Architecture Guidelines
### Widgets
- Avoid function-based widgets like `Widget _someWidget() => Container();`
- Use **private widget classes** within their screen directories:
  ```
  lib/ui/screens/login/widgets/_body.dart  => class _Body extends StatelessWidget
  lib/ui/screens/login/widgets/_header.dart => class _Header extends StatelessWidget
  ```
- If a widget is reused across multiple screens, extract and place it under:
  ```
  lib/ui/widgets/
  ```
- Use named constructors for variants, but:
  - Make sure to have variants as `Enum` not `String` type.
  - Keep the variants style in separate files and use `part` and `part of` declarative to connect files
## Coding Conventions
- Write methods as `getters` if they don't take any parameters and simply return a value.
- UI files should not exceed **200–250 lines**.
  - Break files using `part` / `part of`.
- No single function should exceed **30–50 lines**. Refactor into smaller helpers if needed.
## Enum
- Use `Enum` instead of `String` where needed
- Write `bool` extensions getters for enums every time
For example:
```
enum SomeEnumType {
    type1,
    type2,
}
extenion SomeEnumTypeX on SomeEnumType {
    bool get isType1 => this == type1;
    bool get isType2 => this == type2;
}
```
```
--------------------------------------------------------------------------------
/src/utils/retry.ts:
--------------------------------------------------------------------------------
```typescript
// utils/retry.mts
import {FigmaError, FigmaRateLimitError, FigmaNetworkError} from '../types/errors.js';
export interface RetryOptions {
    maxAttempts?: number;
    initialDelayMs?: number;
    maxDelayMs?: number;
    backoffMultiplier?: number;
    retryableErrors?: (error: Error) => boolean;
}
export interface RetryState {
    attempt: number;
    totalElapsed: number;
    lastError?: Error;
}
const DEFAULT_OPTIONS: Required<RetryOptions> = {
    maxAttempts: 3,
    initialDelayMs: 1000,
    maxDelayMs: 30000,
    backoffMultiplier: 2,
    retryableErrors: (error: Error): boolean => {
        // Retry on network errors and rate limits, but not auth or parse errors
        return (error instanceof FigmaNetworkError) ||
            (error instanceof FigmaRateLimitError) ||
            (error instanceof FigmaError && error.statusCode && error.statusCode >= 500) || false;
    }
};
export async function withRetry<T>(
    operation: () => Promise<T>,
    options: RetryOptions = {}
): Promise<T> {
    const opts = {...DEFAULT_OPTIONS, ...options};
    let lastError: Error;
    for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) {
        try {
            return await operation();
        } catch (error) {
            lastError = error as Error;
            // Don't retry if this is the last attempt
            if (attempt === opts.maxAttempts) {
                break;
            }
            // Don't retry if error is not retryable
            if (!opts.retryableErrors(lastError)) {
                break;
            }
            // Calculate delay
            let delay = opts.initialDelayMs * Math.pow(opts.backoffMultiplier, attempt - 1);
            // Handle rate limit specific delay
            if (lastError instanceof FigmaRateLimitError && lastError.retryAfter) {
                delay = lastError.retryAfter * 1000; // Convert to ms
            }
            // Cap the delay
            delay = Math.min(delay, opts.maxDelayMs);
            console.log(`Attempt ${attempt} failed: ${lastError.message}. Retrying in ${delay}ms...`);
            await sleep(delay);
        }
    }
    throw lastError!;
}
function sleep(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
}
// Helper function for logging retry state
export function logRetryAttempt(state: RetryState, error: Error): void {
    console.warn(`Retry attempt ${state.attempt} failed after ${state.totalElapsed}ms: ${error.message}`);
}
```
--------------------------------------------------------------------------------
/src/extractors/screens/types.ts:
--------------------------------------------------------------------------------
```typescript
// src/extractors/screens/types.mts
import type {FigmaNode, FigmaColor} from '../../types/figma.js';
import type {ComponentChild, NestedComponentInfo, LayoutInfo, StylingInfo} from '../components/types.js';
/**
 * Main screen analysis result
 */
export interface ScreenAnalysis {
    metadata: ScreenMetadata;
    layout: ScreenLayoutInfo;
    sections: ScreenSection[];
    components: NestedComponentInfo[];
    navigation: NavigationInfo;
    assets: ScreenAssetInfo[];
    skippedNodes?: SkippedNodeInfo[];
}
/**
 * Screen metadata information
 */
export interface ScreenMetadata {
    name: string;
    type: 'FRAME' | 'PAGE' | 'COMPONENT';
    nodeId: string;
    description?: string;
    deviceType?: 'mobile' | 'tablet' | 'desktop' | 'unknown';
    orientation?: 'portrait' | 'landscape';
    dimensions: {
        width: number;
        height: number;
    };
}
/**
 * Screen layout information optimized for full screens
 */
export interface ScreenLayoutInfo extends LayoutInfo {
    scrollable?: boolean;
    hasHeader?: boolean;
    hasFooter?: boolean;
    hasNavigation?: boolean;
    contentArea?: {
        x: number;
        y: number;
        width: number;
        height: number;
    };
}
/**
 * Screen section (header, content, footer, etc.)
 */
export interface ScreenSection {
    id: string;
    name: string;
    type: 'header' | 'navigation' | 'content' | 'footer' | 'sidebar' | 'modal' | 'other';
    nodeId: string;
    layout: Partial<LayoutInfo>;
    styling?: Partial<StylingInfo>;
    children: ComponentChild[];
    components: NestedComponentInfo[];
    importance: number; // 1-10 score
}
/**
 * Navigation information
 */
export interface NavigationInfo {
    hasTabBar?: boolean;
    hasAppBar?: boolean;
    hasDrawer?: boolean;
    hasBottomSheet?: boolean;
    navigationElements: NavigationElement[];
}
/**
 * Navigation element
 */
export interface NavigationElement {
    nodeId: string;
    name: string;
    type: 'tab' | 'button' | 'link' | 'icon' | 'menu' | 'other';
    text?: string;
    icon?: boolean;
    isActive?: boolean;
}
/**
 * Screen asset information
 */
export interface ScreenAssetInfo {
    nodeId: string;
    name: string;
    type: 'image' | 'icon' | 'illustration' | 'background';
    size: 'small' | 'medium' | 'large';
    usage: 'decorative' | 'content' | 'navigation' | 'branding';
}
/**
 * Information about nodes that were skipped
 */
export interface SkippedNodeInfo {
    nodeId: string;
    name: string;
    type: string;
    reason: 'depth_limit' | 'complexity' | 'max_sections' | 'device_ui_element';
}
/**
 * Screen extraction options
 */
export interface ScreenExtractionOptions {
    maxSections?: number;
    maxDepth?: number;
    includeHiddenNodes?: boolean;
    extractNavigation?: boolean;
    extractAssets?: boolean;
    deviceTypeDetection?: boolean;
}
```
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
# figma-flutter-mcp
## 0.3.3
### Patch Changes
- [#42](https://github.com/mhmzdev/figma-flutter-mcp/pull/42) [`869314e`](https://github.com/mhmzdev/figma-flutter-mcp/commit/869314e16a2a393ddba22383a8e576161901ab71) Thanks [@mhmzdev](https://github.com/mhmzdev)! - feat: implement advanced style deduplication with semantic matching and auto-optimization
## 0.3.2
### Patch Changes
- [#40](https://github.com/mhmzdev/figma-flutter-mcp/pull/40) [`287dbcc`](https://github.com/mhmzdev/figma-flutter-mcp/commit/287dbcc3a25040fe0f1f05aaf6374472683b8cba) Thanks [@mhmzdev](https://github.com/mhmzdev)! - StreamableHTTP and API key setup improved
## 0.3.1
### Patch Changes
- [#38](https://github.com/mhmzdev/figma-flutter-mcp/pull/38) [`089b764`](https://github.com/mhmzdev/figma-flutter-mcp/commit/089b7647d5e4e37452b973ba521fb3bc878427dd) Thanks [@mhmzdev](https://github.com/mhmzdev)! - Docs updated with more understanding
## 0.3.0
### Minor Changes
- [#36](https://github.com/mhmzdev/figma-flutter-mcp/pull/36) [`d3e64e8`](https://github.com/mhmzdev/figma-flutter-mcp/commit/d3e64e8c3a64d07deed0c2102b79bd9e2c51cfc3) Thanks [@mhmzdev](https://github.com/mhmzdev)! - Migrated .mts to .ts, added semantic detection and visual context approaches
## 0.2.0
### Minor Changes
- [#33](https://github.com/mhmzdev/figma-flutter-mcp/pull/33) [`990dfc6`](https://github.com/mhmzdev/figma-flutter-mcp/commit/990dfc6070a64131182df9371ad94b7d5fb114ab) Thanks [@mhmzdev](https://github.com/mhmzdev)! - Break-even point: ~22 component analyses
## 0.1.8
### Patch Changes
- [#28](https://github.com/mhmzdev/figma-flutter-mcp/pull/28) [`cc3c184`](https://github.com/mhmzdev/figma-flutter-mcp/commit/cc3c18489fc40bb9849671710b0b97dd95bc2a31) Thanks [@mhmzdev](https://github.com/mhmzdev)! - Getting started docs added, Demo video added
## 0.1.7
### Patch Changes
- [#25](https://github.com/mhmzdev/figma-flutter-mcp/pull/25) [`4d438a9`](https://github.com/mhmzdev/figma-flutter-mcp/commit/4d438a95f6ba703a971b2c5dceb0af8d245d78c8) Thanks [@mhmzdev](https://github.com/mhmzdev)! - Docs updated for local setup
## 0.1.6
### Patch Changes
- [#22](https://github.com/mhmzdev/figma-flutter-mcp/pull/22) [`fffb12a`](https://github.com/mhmzdev/figma-flutter-mcp/commit/fffb12ab10b281f632a6506c5cb5053a039c57e7) Thanks [@mhmzdev](https://github.com/mhmzdev)! - Figma API key logic improved for API access
## 0.1.5
### Patch Changes
- [`611ee64`](https://github.com/mhmzdev/figma-flutter-mcp/commit/611ee646d684925808ae9191ba851c9a86cb1d7b) Thanks [@mhmzdev](https://github.com/mhmzdev)! - Removed empty files so that AI model doesn't get confused
## 0.1.4
### Patch Changes
- [#19](https://github.com/mhmzdev/figma-flutter-mcp/pull/19) [`5226c25`](https://github.com/mhmzdev/figma-flutter-mcp/commit/5226c250a19b96d4664ab80805f3efac7c0c4b75) Thanks [@mhmzdev](https://github.com/mhmzdev)! - Fixed the TYPO for API key that was causing a crash
## 0.1.3
### Patch Changes
- [#15](https://github.com/mhmzdev/figma-flutter-mcp/pull/15) [`692dd43`](https://github.com/mhmzdev/figma-flutter-mcp/commit/692dd43b364f50055cf577715dd7921e430f05a1) Thanks [@mhmzdev](https://github.com/mhmzdev)! - Added workflows for better CI/CD
```
--------------------------------------------------------------------------------
/src/extractors/components/core.ts:
--------------------------------------------------------------------------------
```typescript
// src/extractors/components/core.mts
import type {FigmaNode} from '../../types/figma.js';
import type {
    ComponentAnalysis,
    ComponentExtractionOptions,
    ComponentVariant
} from './types.js';
import {
    extractMetadata,
    extractLayoutInfo,
    extractStylingInfo,
    analyzeChildren,
} from './extractor.js';
/**
 * Component extraction and analysis class
 */
export class ComponentExtractor {
    private options: Required<ComponentExtractionOptions>;
    constructor(options: ComponentExtractionOptions = {}) {
        this.options = {
            maxChildNodes: options.maxChildNodes ?? 10,
            maxDepth: options.maxDepth ?? 3,
            includeHiddenNodes: options.includeHiddenNodes ?? false,
            prioritizeComponents: options.prioritizeComponents ?? true,
            extractTextContent: options.extractTextContent ?? true
        };
    }
    /**
     * Main component analysis method
     */
    async analyzeComponent(node: FigmaNode, userDefinedAsComponent: boolean = false): Promise<ComponentAnalysis> {
        const metadata = extractMetadata(node, userDefinedAsComponent);
        const layout = extractLayoutInfo(node);
        const styling = extractStylingInfo(node);
        const {children, nestedComponents, skippedNodes} = analyzeChildren(node, this.options);
        return {
            metadata,
            layout,
            styling,
            children,
            nestedComponents,
            skippedNodes: skippedNodes.length > 0 ? skippedNodes : undefined
        };
    }
    /**
     * Get extractor options
     */
    getOptions(): Required<ComponentExtractionOptions> {
        return this.options;
    }
}
/**
 * Convenience function to analyze a single component
 */
export async function analyzeComponent(
    node: FigmaNode,
    options: ComponentExtractionOptions = {},
    userDefinedAsComponent: boolean = false
): Promise<ComponentAnalysis> {
    const extractor = new ComponentExtractor(options);
    return extractor.analyzeComponent(node, userDefinedAsComponent);
}
/**
 * Convenience function to analyze component with variants
 */
export async function analyzeComponentWithVariants(
    componentSetNode: FigmaNode,
    options: ComponentExtractionOptions = {}
): Promise<{
    variants: ComponentVariant[];
    defaultAnalysis?: ComponentAnalysis;
}> {
    if (componentSetNode.type !== 'COMPONENT_SET') {
        throw new Error('Node is not a COMPONENT_SET');
    }
    const {VariantAnalyzer} = await import('./variant-analyzer.js');
    const variantAnalyzer = new VariantAnalyzer();
    const variants = await variantAnalyzer.analyzeComponentSet(componentSetNode);
    // Find and analyze the default variant
    const defaultVariant = variants.find(v => v.isDefault);
    let defaultAnalysis: ComponentAnalysis | undefined;
    if (defaultVariant && componentSetNode.children) {
        const defaultVariantNode = componentSetNode.children.find(child => child.id === defaultVariant.nodeId);
        if (defaultVariantNode) {
            const extractor = new ComponentExtractor(options);
            defaultAnalysis = await extractor.analyzeComponent(defaultVariantNode, false);
        }
    }
    return {
        variants,
        defaultAnalysis
    };
}
```
--------------------------------------------------------------------------------
/src/tools/flutter/theme/colors/theme-generator.ts:
--------------------------------------------------------------------------------
```typescript
// src/tools/flutter/simple-theme-generator.mts
import {writeFile, mkdir} from 'fs/promises';
import {join} from 'path';
import type {ThemeColor, ThemeGenerationOptions} from '../../../../extractors/colors/index.js';
export class SimpleThemeGenerator {
    /**
     * Generate AppColors Dart class from theme colors
     */
    async generateAppColors(colors: ThemeColor[], outputPath: string, options: ThemeGenerationOptions = {}): Promise<string> {
        // Create output directory
        await mkdir(outputPath, {recursive: true});
        const filePath = join(outputPath, 'app_colors.dart');
        // Generate Dart content
        const content = this.generateDartContent(colors);
        await writeFile(filePath, content);
        // Generate ThemeData if requested
        if (options.generateThemeData) {
            await this.generateThemeData(colors, outputPath, options);
        }
        return filePath;
    }
    /**
     * Generate Flutter ThemeData from theme colors
     */
    async generateThemeData(colors: ThemeColor[], outputPath: string, options: ThemeGenerationOptions = {}): Promise<string> {
        const filePath = join(outputPath, 'app_theme.dart');
        const content = this.generateThemeDataContent(colors, options);
        await writeFile(filePath, content);
        return filePath;
    }
    private generateDartContent(colors: ThemeColor[]): string {
        let content = `// Generated AppColors from Figma theme frame
import 'package:flutter/material.dart';
class AppColors {
`;
        // Generate color constants
        colors.forEach(color => {
            const constantName = this.toDartConstantName(color.name);
            content += `  /// ${color.name}
  static const Color ${constantName} = Color(0xFF${color.hex.substring(1)});
`;
        });
        content += `}\n`;
        return content;
    }
    private generateThemeDataContent(colors: ThemeColor[], options: ThemeGenerationOptions): string {
        const timestamp = new Date().toISOString().split('T')[0];
        const colorMap = this.createColorMap(colors);
        let content = `// Generated Flutter ThemeData from Figma theme frame
import 'package:flutter/material.dart';
import 'app_colors.dart';
class AppTheme {
  // Light Theme
  static ThemeData get lightTheme {
    return ThemeData(
      useMaterial3: true,
      brightness: Brightness.light,
`;
        // Add ColorScheme if requested
        if (options.includeColorScheme !== false) {
            content += this.generateColorScheme(colorMap);
        }
        content += `    );
  }
}
`;
        return content;
    }
    private createColorMap(colors: ThemeColor[]): Map<string, string> {
        const colorMap = new Map<string, string>();
        colors.forEach(color => {
            const key = color.name.toLowerCase().replace(/\s+/g, '');
            colorMap.set(key, `AppColors.${this.toDartConstantName(color.name)}`);
        });
        return colorMap;
    }
    private generateColorScheme(colorMap: Map<string, string>): string {
        const primary = colorMap.get('primary') || 'AppColors.primary';
        const secondary = colorMap.get('secondary') || colorMap.get('accent') || 'Colors.blue.shade300';
        const background = colorMap.get('background') || colorMap.get('backgroundlight') || 'Colors.white';
        const surface = colorMap.get('surface') || background;
        const error = colorMap.get('error') || colorMap.get('danger') || 'Colors.red';
        return `      colorScheme: ColorScheme.fromSeed(
        seedColor: ${primary},
        brightness: Brightness.light,
        primary: ${primary},
        secondary: ${secondary},
        background: ${background},
        surface: ${surface},
        error: ${error},
      ),
`;
    }
    private toDartConstantName(name: string): string {
        // Convert to camelCase for Dart constants
        return name
            .replace(/[^a-zA-Z0-9]/g, ' ')
            .split(' ')
            .filter(word => word.length > 0)
            .map((word, index) => {
                if (index === 0) {
                    return word.toLowerCase();
                }
                return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
            })
            .join('');
    }
}
```
--------------------------------------------------------------------------------
/src/extractors/components/types.ts:
--------------------------------------------------------------------------------
```typescript
// src/extractors/components/types.mts
import type {FigmaNode, FigmaColor, FigmaEffect} from '../../types/figma.js';
/**
 * Main component analysis result
 */
export interface ComponentAnalysis {
    metadata: ComponentMetadata;
    layout: LayoutInfo;
    styling: StylingInfo;
    children: ComponentChild[];
    nestedComponents: NestedComponentInfo[];
    variants?: ComponentVariant[];
    skippedNodes?: SkippedNodeInfo[];
}
/**
 * Component metadata information
 */
export interface ComponentMetadata {
    name: string;
    type: 'COMPONENT' | 'COMPONENT_SET' | 'FRAME';
    nodeId: string;
    description?: string;
    variantCount?: number;
    isUserDefinedComponent?: boolean; // When user treats FRAME as component
    componentKey?: string; // For actual Figma components
}
/**
 * Layout structure information
 */
export interface LayoutInfo {
    type: 'auto-layout' | 'absolute' | 'frame';
    direction?: 'horizontal' | 'vertical';
    spacing?: number;
    padding?: PaddingInfo;
    constraints?: any;
    dimensions: {
        width: number;
        height: number;
    };
    alignItems?: string;
    justifyContent?: string;
}
/**
 * Padding information
 */
export interface PaddingInfo {
    top: number;
    right: number;
    bottom: number;
    left: number;
    isUniform: boolean;
}
/**
 * Visual styling information
 */
export interface StylingInfo {
    fills?: ColorInfo[];
    strokes?: StrokeInfo[];
    effects?: CategorizedEffects;
    cornerRadius?: number | CornerRadii;
    opacity?: number;
}
/**
 * Color information extracted from fills
 */
export interface ColorInfo {
    type: string;
    color?: FigmaColor;
    hex?: string;
    opacity?: number;
    gradientStops?: Array<{
        color: FigmaColor;
        position: number;
    }>;
}
/**
 * Stroke information
 */
export interface StrokeInfo {
    type: string;
    color: FigmaColor;
    hex: string;
    weight: number;
    align?: string;
}
/**
 * Corner radius information
 */
export interface CornerRadii {
    topLeft: number;
    topRight: number;
    bottomLeft: number;
    bottomRight: number;
    isUniform: boolean;
}
/**
 * Categorized effects for Flutter mapping
 */
export interface CategorizedEffects {
    dropShadows: DropShadowEffect[];
    innerShadows: InnerShadowEffect[];
    blurs: BlurEffect[];
}
/**
 * Drop shadow effect
 */
export interface DropShadowEffect {
    color: FigmaColor;
    hex: string;
    offset: {x: number; y: number};
    radius: number;
    spread?: number;
    opacity: number;
}
/**
 * Inner shadow effect
 */
export interface InnerShadowEffect {
    color: FigmaColor;
    hex: string;
    offset: {x: number; y: number};
    radius: number;
    spread?: number;
    opacity: number;
}
/**
 * Blur effect
 */
export interface BlurEffect {
    type: string;
    radius: number;
}
/**
 * Child component information
 */
export interface ComponentChild {
    nodeId: string;
    name: string;
    type: string;
    isNestedComponent: boolean;
    visualImportance: number; // 1-10 score for prioritization
    basicInfo?: {
        layout?: Partial<LayoutInfo>;
        styling?: Partial<StylingInfo>;
        text?: TextInfo;
    };
}
/**
 * Enhanced text-specific information
 */
export interface TextInfo {
    content: string;
    isPlaceholder: boolean;
    fontFamily?: string;
    fontSize?: number;
    fontWeight?: number;
    textAlign?: string;
    textCase?: 'uppercase' | 'lowercase' | 'capitalize' | 'sentence' | 'mixed';
    semanticType?: 'heading' | 'body' | 'label' | 'button' | 'link' | 'caption' | 'error' | 'success' | 'warning' | 'other';
    placeholder?: boolean; // Flag for Flutter implementation
}
/**
 * Nested component that should be analyzed separately
 */
export interface NestedComponentInfo {
    nodeId: string;
    name: string;
    componentKey?: string;
    masterComponent?: string;
    isComponentInstance: boolean;
    needsSeparateAnalysis: boolean;
    instanceType?: 'COMPONENT' | 'COMPONENT_SET';
}
/**
 * Component variant information
 */
export interface ComponentVariant {
    nodeId: string;
    name: string;
    properties: Record<string, string>;
    isDefault: boolean;
}
/**
 * Information about nodes that were skipped due to limits
 */
export interface SkippedNodeInfo {
    nodeId: string;
    name: string;
    type: string;
    reason: 'depth_limit' | 'visual_importance' | 'max_nodes';
}
/**
 * Component extraction options
 */
export interface ComponentExtractionOptions {
    maxChildNodes?: number;
    maxDepth?: number;
    includeHiddenNodes?: boolean;
    prioritizeComponents?: boolean;
    extractTextContent?: boolean;
}
```
--------------------------------------------------------------------------------
/src/types/figma.ts:
--------------------------------------------------------------------------------
```typescript
// src/types/figma.mts
export interface FigmaFile {
    document: FigmaNode;
    components: {[key: string]: FigmaComponent};
    styles: {[key: string]: FigmaStyle};
    name: string;
    lastModified: string;
    version: string;
    role: string;
    editorType: string;
}
export interface FigmaNode {
    id: string;
    name: string;
    type: string;
    visible?: boolean;
    children?: FigmaNode[];
    fills?: FigmaFill[];
    strokes?: FigmaStroke[];
    effects?: FigmaEffect[];
    backgroundColor?: FigmaColor;
    style?: FigmaTextStyle;
    constraints?: FigmaConstraints;
    absoluteBoundingBox?: FigmaBoundingBox;
    layoutMode?: string;
    primaryAxisSizingMode?: string;
    counterAxisSizingMode?: string;
    paddingLeft?: number;
    paddingRight?: number;
    paddingTop?: number;
    paddingBottom?: number;
    itemSpacing?: number;
    // Text-specific properties
    characters?: string; // Actual text content for TEXT nodes
    characterStyleOverrides?: number[];
    styleOverrideTable?: {[key: string]: FigmaTextStyle};
}
export interface FigmaComponent {
    key: string;
    file_key: string;
    node_id: string;
    thumbnail_url: string;
    name: string;
    description: string;
    created_at: string;
    updated_at: string;
    user: {
        id: string;
        handle: string;
        img_url: string;
    };
    containing_frame?: {
        name: string;
        node_id: string;
    };
}
export interface FigmaComponentSet {
    key: string;
    file_key: string;
    node_id: string;
    thumbnail_url: string;
    name: string;
    description: string;
    created_at: string;
    updated_at: string;
    user: {
        id: string;
        handle: string;
        img_url: string;
    };
    containing_frame?: {
        name: string;
        node_id: string;
    };
}
export interface FigmaStyle {
    key: string;
    file_key: string;
    node_id: string;
    style_type: 'FILL' | 'TEXT' | 'EFFECT' | 'GRID';
    thumbnail_url: string;
    name: string;
    description: string;
    created_at: string;
    updated_at: string;
    user: {
        id: string;
        handle: string;
        img_url: string;
    };
    sort_position: string;
}
export interface FigmaColor {
    r: number;
    g: number;
    b: number;
    a: number;
}
export interface FigmaFill {
    type: string;
    color?: FigmaColor;
    gradientStops?: Array<{
        color: FigmaColor;
        position: number;
    }>;
    visible?: boolean;
}
export interface FigmaStroke {
    type: string;
    color: FigmaColor;
    strokeWeight?: number;
    visible?: boolean;
}
export interface FigmaEffect {
    type: string;
    color?: FigmaColor;
    offset?: {x: number; y: number};
    radius: number;
    spread?: number;
    visible?: boolean;
}
export interface FigmaTextStyle {
    fontFamily: string;
    fontWeight: number;
    fontSize: number;
    letterSpacing: number;
    lineHeightPx: number;
    textAlignHorizontal: string;
    textAlignVertical: string;
}
export interface FigmaConstraints {
    vertical: string;
    horizontal: string;
}
export interface FigmaBoundingBox {
    x: number;
    y: number;
    width: number;
    height: number;
}
export interface FigmaPageInfo {
    id: string;
    name: string;
    type: string;
}
export interface FigmaFileInfo {
    name: string;
    lastModified: string;
    version: string;
    role: string;
    editorType: string;
    componentCount: number;
    styleCount: number;
    pageCount: number;
}
// Image Export Types
export interface ImageExportOptions {
    format?: 'png' | 'jpg' | 'svg' | 'pdf';
    scale?: number; // 1, 2, 3, 4 for PNG/JPG
    svgIncludeId?: boolean;
    svgSimplifyStroke?: boolean;
    svgOutlineText?: boolean;
    useAbsoluteBounds?: boolean;
    version?: string;
}
export interface ImageExportResponse {
    err?: string;
    images: {[nodeId: string]: string | null};
}
export interface ImageFillsResponse {
    err?: string;
    meta: {
        images: {[imageRef: string]: string};
    };
}
// Responses
export interface ComponentResponse {
    meta: {
        components: FigmaComponent[];
    };
}
export interface ComponentSetResponse {
    meta: {
        component_sets: FigmaComponentSet[];
    };
}
export interface StylesResponse {
    meta: {
        styles: FigmaStyle[];
    };
}
// Single Node Response
export interface NodeResponse {
    nodes: {
        [nodeId: string]: {
            document: FigmaNode;
            components?: {[key: string]: FigmaComponent};
            styles?: {[key: string]: FigmaStyle};
        };
    };
}
// Page Response  
export interface PageResponse {
    document: FigmaNode;
    components?: {[key: string]: FigmaComponent};
    styles?: {[key: string]: FigmaStyle};
}
```
--------------------------------------------------------------------------------
/src/extractors/colors/core.ts:
--------------------------------------------------------------------------------
```typescript
// src/extractors/colors/core.mts
import type {FigmaNode} from '../../types/figma.js';
import type {
    ThemeColor,
    ColorDefinition,
    ColorExtractionContext,
    ColorExtractorFn,
} from './types.js';
import {
    extractColorsFromThemeFrame,
} from './extractor.js';
/**
 * Main color extraction orchestrator
 */
export class ColorExtractor {
    private colorLibrary: ColorDefinition[] = [];
    private colorMap = new Map<string, string>(); // color value -> ID
    /**
     * Extract theme colors from a specific frame
     */
    extractThemeFromFrame(frameNode: FigmaNode): ThemeColor[] {
        return extractColorsFromThemeFrame(frameNode);
    }
    /**
     * Extract colors from a single node recursively
     */
    private extractFromNode(
        node: FigmaNode,
        extractors: ColorExtractorFn[],
        context: ColorExtractionContext
    ): void {
        if (context.currentDepth > context.maxDepth) {
            return;
        }
        // Apply all color extractors to this node
        extractors.forEach(extractor => {
            extractor(node, context, this.colorLibrary);
        });
        // Process children
        if (node.children && node.children.length > 0) {
            const childContext = {...context, currentDepth: context.currentDepth + 1};
            node.children.forEach(child => {
                this.extractFromNode(child, extractors, childContext);
            });
        }
    }
    /**
     * Add or get existing color from library
     */
    addColor(colorValue: string, nodeName: string): string {
        // Check if color already exists
        const existingId = this.colorMap.get(colorValue);
        if (existingId) {
            // Increment usage count
            const color = this.colorLibrary.find(c => c.id === existingId);
            if (color) {
                color.usageCount++;
            }
            return existingId;
        }
        // Create new color definition
        const colorId = `color_${this.colorLibrary.length + 1}`;
        const colorDef: ColorDefinition = {
            id: colorId,
            name: this.generateColorName(colorValue, nodeName),
            value: colorValue,
            usage: this.categorizeColorUsage(nodeName),
            usageCount: 1
        };
        this.colorLibrary.push(colorDef);
        this.colorMap.set(colorValue, colorId);
        return colorId;
    }
    /**
     * Get current color library
     */
    getColorLibrary(): ColorDefinition[] {
        return [...this.colorLibrary];
    }
    /**
     * Get colors by usage category
     */
    getColorsByUsage(usage: ColorDefinition['usage']): ColorDefinition[] {
        return this.colorLibrary.filter(color => color.usage === usage);
    }
    /**
     * Get most used colors
     */
    getMostUsedColors(limit: number = 10): ColorDefinition[] {
        return [...this.colorLibrary]
            .sort((a, b) => b.usageCount - a.usageCount)
            .slice(0, limit);
    }
    /**
     * Reset all libraries for new extraction
     */
    private resetLibraries(): void {
        this.colorLibrary = [];
        this.colorMap.clear();
    }
    /**
     * Generate meaningful color name from hex value and context
     */
    private generateColorName(colorValue: string, nodeName: string): string {
        // Try to infer name from node context
        const name = nodeName.toLowerCase();
        if (name.includes('primary')) return 'Primary';
        if (name.includes('secondary')) return 'Secondary';
        if (name.includes('background')) return 'Background';
        if (name.includes('text')) return 'Text';
        if (name.includes('accent')) return 'Accent';
        // Generate name based on color value
        const colorNames: Record<string, string> = {
            '#ffffff': 'White',
            '#000000': 'Black',
            '#ff0000': 'Red',
            '#00ff00': 'Green',
            '#0000ff': 'Blue',
        };
        return colorNames[colorValue.toLowerCase()] || `Color${this.colorLibrary.length + 1}`;
    }
    /**
     * Categorize color usage
     */
    private categorizeColorUsage(nodeName: string): ColorDefinition['usage'] {
        const name = nodeName.toLowerCase();
        if (name.includes('primary')) return 'primary';
        if (name.includes('secondary')) return 'secondary';
        if (name.includes('background') || name.includes('bg')) return 'background';
        if (name.includes('text') || name.includes('label')) return 'text';
        if (name.includes('accent') || name.includes('highlight')) return 'accent';
        return 'other';
    }
}
/**
 * Convenience function to extract theme colors from a frame
 */
export function extractThemeColors(frameNode: FigmaNode): ThemeColor[] {
    const extractor = new ColorExtractor();
    return extractor.extractThemeFromFrame(frameNode);
}
```
--------------------------------------------------------------------------------
/src/utils/validation.ts:
--------------------------------------------------------------------------------
```typescript
// utils/validation.mts
import {FigmaError} from '../types/errors.js';
/**
 * Utility class for validating Figma-related inputs
 */
export class FigmaValidator {
    /**
     * Validate Figma file ID
     * @param fileId The file ID to validate
     * @param fieldName Optional field name for error messages
     */
    static validateFileId(fileId: unknown, fieldName = 'fileId'): string {
        if (typeof fileId !== 'string') {
            throw new FigmaError(`${fieldName} must be a string`, 'VALIDATION_ERROR');
        }
        const trimmed = fileId.trim();
        if (trimmed.length === 0) {
            throw new FigmaError(`${fieldName} cannot be empty`, 'VALIDATION_ERROR');
        }
        if (trimmed.length < 10 || trimmed.length > 50) {
            throw new FigmaError(
                `${fieldName} length must be between 10-50 characters, got ${trimmed.length}`,
                'VALIDATION_ERROR'
            );
        }
        const validPattern = /^[a-zA-Z0-9\-_]+$/;
        if (!validPattern.test(trimmed)) {
            throw new FigmaError(
                `${fieldName} contains invalid characters. Only alphanumeric, hyphens, and underscores allowed`,
                'VALIDATION_ERROR'
            );
        }
        return trimmed;
    }
    /**
     * Validate Figma node ID
     * @param nodeId The node ID to validate
     * @param fieldName Optional field name for error messages
     */
    static validateNodeId(nodeId: unknown, fieldName = 'nodeId'): string {
        if (typeof nodeId !== 'string') {
            throw new FigmaError(`${fieldName} must be a string`, 'VALIDATION_ERROR');
        }
        const trimmed = nodeId.trim();
        if (trimmed.length === 0) {
            throw new FigmaError(`${fieldName} cannot be empty`, 'VALIDATION_ERROR');
        }
        // Figma node IDs are typically in format "number:number"
        const nodePattern = /^\d+:\d+$/;
        if (!nodePattern.test(trimmed)) {
            throw new FigmaError(
                `${fieldName} must be in format "123:456", got "${trimmed}"`,
                'VALIDATION_ERROR'
            );
        }
        return trimmed;
    }
    /**
     * Validate depth parameter
     * @param depth The depth to validate
     * @param fieldName Optional field name for error messages
     */
    static validateDepth(depth: unknown, fieldName = 'depth'): number {
        if (depth === undefined || depth === null) {
            return 3; // Default depth
        }
        if (typeof depth !== 'number' || !Number.isInteger(depth)) {
            throw new FigmaError(`${fieldName} must be an integer`, 'VALIDATION_ERROR');
        }
        if (depth < 1 || depth > 10) {
            throw new FigmaError(
                `${fieldName} must be between 1-10, got ${depth}`,
                'VALIDATION_ERROR'
            );
        }
        return depth;
    }
    /**
     * Validate max components parameter
     * @param maxComponents The max components to validate
     * @param fieldName Optional field name for error messages
     */
    static validateMaxComponents(maxComponents: unknown, fieldName = 'maxComponents'): number {
        if (maxComponents === undefined || maxComponents === null) {
            return 10; // Default max components
        }
        if (typeof maxComponents !== 'number' || !Number.isInteger(maxComponents)) {
            throw new FigmaError(`${fieldName} must be an integer`, 'VALIDATION_ERROR');
        }
        if (maxComponents < 1 || maxComponents > 100) {
            throw new FigmaError(
                `${fieldName} must be between 1-100, got ${maxComponents}`,
                'VALIDATION_ERROR'
            );
        }
        return maxComponents;
    }
    /**
     * Validate and sanitize widget name for Flutter
     * @param name The widget name to validate
     * @param fieldName Optional field name for error messages
     */
    static validateWidgetName(name: unknown, fieldName = 'widgetName'): string {
        if (typeof name !== 'string') {
            throw new FigmaError(`${fieldName} must be a string`, 'VALIDATION_ERROR');
        }
        const trimmed = name.trim();
        if (trimmed.length === 0) {
            throw new FigmaError(`${fieldName} cannot be empty`, 'VALIDATION_ERROR');
        }
        // Check for valid Dart/Flutter class name
        const validDartName = /^[A-Z][a-zA-Z0-9_]*$/;
        if (!validDartName.test(trimmed)) {
            throw new FigmaError(
                `${fieldName} "${trimmed}" is not a valid Flutter class name. Must start with uppercase letter and contain only letters, numbers, and underscores`,
                'VALIDATION_ERROR'
            );
        }
        return trimmed;
    }
    /**
     * Comprehensive validation for tool inputs
     * @param inputs Object containing inputs to validate
     */
    static validateToolInputs(inputs: {
        fileId?: unknown;
        nodeId?: unknown;
        depth?: unknown;
        maxComponents?: unknown;
        widgetName?: unknown;
    }): {
        fileId?: string;
        nodeId?: string;
        depth?: number;
        maxComponents?: number;
        widgetName?: string;
    } {
        const result: any = {};
        if (inputs.fileId !== undefined) {
            result.fileId = this.validateFileId(inputs.fileId);
        }
        if (inputs.nodeId !== undefined) {
            result.nodeId = this.validateNodeId(inputs.nodeId);
        }
        if (inputs.depth !== undefined) {
            result.depth = this.validateDepth(inputs.depth);
        }
        if (inputs.maxComponents !== undefined) {
            result.maxComponents = this.validateMaxComponents(inputs.maxComponents);
        }
        if (inputs.widgetName !== undefined) {
            result.widgetName = this.validateWidgetName(inputs.widgetName);
        }
        return result;
    }
}
```
--------------------------------------------------------------------------------
/src/extractors/components/deduplicated-extractor.ts:
--------------------------------------------------------------------------------
```typescript
// src/extractors/components/deduplicated-extractor.mts
import type { FigmaNode } from '../../types/figma.js';
import type { FlutterStyleDefinition } from '../flutter/style-library.js';
import { FlutterStyleLibrary } from '../flutter/style-library.js';
import { GlobalStyleManager } from '../flutter/global-vars.js';
import { 
  extractStylingInfo, 
  extractLayoutInfo, 
  extractMetadata,
  extractTextInfo,
  createNestedComponentInfo
} from './extractor.js';
import type {
  ComponentMetadata,
  LayoutInfo,
  StylingInfo,
  NestedComponentInfo,
  TextInfo
} from './types.js';
export interface DeduplicatedComponentAnalysis {
  metadata: ComponentMetadata;
  styleRefs: Record<string, string>;
  children: DeduplicatedComponentChild[];
  nestedComponents: NestedComponentInfo[];
  newStyleDefinitions?: Record<string, FlutterStyleDefinition>;
}
export interface DeduplicatedComponentChild {
  nodeId: string;
  name: string;
  type: string;
  styleRefs: string[];
  semanticType?: string;
  textContent?: string;
}
export class DeduplicatedComponentExtractor {
  private styleLibrary = FlutterStyleLibrary.getInstance();
  private globalStyleManager = new GlobalStyleManager();
  
  async analyzeComponent(node: FigmaNode, trackNewStyles = false): Promise<DeduplicatedComponentAnalysis> {
    const styling = extractStylingInfo(node);
    const layout = extractLayoutInfo(node);
    const metadata = extractMetadata(node, false); // assuming not user-defined unless specified
    
    const styleRefs: Record<string, string> = {};
    const newStyles = new Set<string>();
    
    // Process decoration styles using the enhanced global style manager
    if (this.hasDecorationProperties(styling)) {
      const beforeCount = this.styleLibrary.getAllStyles().length;
      styleRefs.decoration = this.globalStyleManager.addStyle({
        fills: styling.fills,
        cornerRadius: styling.cornerRadius,
        effects: styling.effects
      }, 'decoration');
      if (trackNewStyles && this.styleLibrary.getAllStyles().length > beforeCount) {
        newStyles.add(styleRefs.decoration);
      }
    }
    
    // Process padding styles using the enhanced global style manager
    if (layout.padding) {
      const beforeCount = this.styleLibrary.getAllStyles().length;
      styleRefs.padding = this.globalStyleManager.addStyle({ padding: layout.padding }, 'padding');
      if (trackNewStyles && this.styleLibrary.getAllStyles().length > beforeCount) {
        newStyles.add(styleRefs.padding);
      }
    }
    
    // Process children with deduplication
    const children = await this.analyzeChildren(node);
    const nestedComponents = this.extractNestedComponents(node);
    
    const result: DeduplicatedComponentAnalysis = {
      metadata,
      styleRefs,
      children,
      nestedComponents
    };
    
    if (trackNewStyles && newStyles.size > 0) {
      result.newStyleDefinitions = this.getStyleDefinitions(Array.from(newStyles));
    }
    
    return result;
  }
  
  private async analyzeChildren(node: FigmaNode): Promise<DeduplicatedComponentChild[]> {
    if (!node.children) return [];
    
    const children: DeduplicatedComponentChild[] = [];
    
    for (const child of node.children) {
      if (!child.visible) continue;
      
      const childStyleRefs: string[] = [];
      
      // Extract child styling using enhanced global style manager
      const childStyling = extractStylingInfo(child);
      if (this.hasDecorationProperties(childStyling)) {
        const decorationRef = this.globalStyleManager.addStyle({
          fills: childStyling.fills,
          cornerRadius: childStyling.cornerRadius,
          effects: childStyling.effects
        }, 'decoration');
        childStyleRefs.push(decorationRef);
      }
      
      // Extract text styling for text nodes using enhanced global style manager
      let textContent: string | undefined;
      if (child.type === 'TEXT') {
        const textInfo = extractTextInfo(child);
        if (textInfo) {
          textContent = textInfo.content;
          
          // Add text style to library using enhanced deduplication
          if (child.style) {
            const textStyleRef = this.globalStyleManager.addStyle({
              fontFamily: child.style.fontFamily,
              fontSize: child.style.fontSize,
              fontWeight: child.style.fontWeight
            }, 'text');
            childStyleRefs.push(textStyleRef);
          }
        }
      }
      
      children.push({
        nodeId: child.id,
        name: child.name,
        type: child.type,
        styleRefs: childStyleRefs,
        semanticType: this.detectSemanticType(child),
        textContent
      });
    }
    
    return children;
  }
  
  private hasDecorationProperties(styling: StylingInfo): boolean {
    return !!(styling.fills?.length || styling.cornerRadius !== undefined || styling.effects?.dropShadows?.length);
  }
  
  private extractTextContent(node: any): string {
    return node.characters || node.name || '';
  }
  
  private detectSemanticType(node: any): string | undefined {
    // Simplified semantic detection
    if (node.type === 'TEXT') {
      const content = this.extractTextContent(node).toLowerCase();
      if (['click', 'submit', 'save', 'cancel'].some(word => content.includes(word))) {
        return 'button';
      }
      return 'text';
    }
    return undefined;
  }
  
  private extractNestedComponents(node: FigmaNode): NestedComponentInfo[] {
    if (!node.children) return [];
    
    const nestedComponents: NestedComponentInfo[] = [];
    
    for (const child of node.children) {
      if (child.type === 'COMPONENT' || child.type === 'INSTANCE' || child.type === 'COMPONENT_SET') {
        nestedComponents.push(createNestedComponentInfo(child));
      }
    }
    
    return nestedComponents;
  }
  
  private getStyleDefinitions(styleIds: string[]): Record<string, FlutterStyleDefinition> {
    const definitions: Record<string, FlutterStyleDefinition> = {};
    styleIds.forEach(id => {
      const definition = this.styleLibrary.getStyle(id);
      if (definition) {
        definitions[id] = definition;
      }
    });
    return definitions;
  }
}
```
--------------------------------------------------------------------------------
/src/figma-config.ts:
--------------------------------------------------------------------------------
```typescript
// figma-config.mts (enhanced version)
import {FigmaError} from './types/errors.js';
export interface FigmaConfig {
    readonly fileId: string;
    readonly defaultPageName: string;
    readonly maxDepth: number;
    readonly maxComponents: number;
    readonly commonNodes: Record<string, string>;
}
export interface FigmaConfigOptions {
    fileId?: string;
    defaultPageName?: string;
    maxDepth?: number;
    maxComponents?: number;
    commonNodes?: Record<string, string>;
}
// Default configuration
const DEFAULT_CONFIG: Omit<FigmaConfig, 'fileId'> = {
    defaultPageName: 'Design',
    maxDepth: 3,
    maxComponents: 10,
    commonNodes: {}
};
class FigmaConfigManager {
    private config: FigmaConfig;
    constructor(options: FigmaConfigOptions = {}) {
        // Try to get fileId from multiple sources
        const fileId = options.fileId ||
            process.env.FIGMA_FILE_ID ||
            '83dXk35avf0BTHtYPWeyl7'; // fallback to your current ID
        this.validateFileId(fileId);
        this.config = {
            fileId,
            defaultPageName: options.defaultPageName || DEFAULT_CONFIG.defaultPageName,
            maxDepth: options.maxDepth || DEFAULT_CONFIG.maxDepth,
            maxComponents: options.maxComponents || DEFAULT_CONFIG.maxComponents,
            commonNodes: {...DEFAULT_CONFIG.commonNodes, ...(options.commonNodes || {})}
        };
    }
    /**
     * Validate Figma file ID format
     * Figma file IDs are typically 22 characters, alphanumeric
     */
    private validateFileId(fileId: string): void {
        if (!fileId || typeof fileId !== 'string') {
            throw new FigmaError('Figma file ID is required', 'INVALID_CONFIG');
        }
        // Remove any whitespace
        fileId = fileId.trim();
        if (fileId.length === 0) {
            throw new FigmaError('Figma file ID cannot be empty', 'INVALID_CONFIG');
        }
        // Figma file IDs are typically 22 characters, alphanumeric
        if (fileId.length < 10 || fileId.length > 50) {
            throw new FigmaError(
                `Invalid Figma file ID length: ${fileId.length}. Expected 10-50 characters.`,
                'INVALID_CONFIG'
            );
        }
        // Check for valid characters (alphanumeric and some special chars)
        const validPattern = /^[a-zA-Z0-9\-_]+$/;
        if (!validPattern.test(fileId)) {
            throw new FigmaError(
                'Invalid Figma file ID format. Only alphanumeric characters, hyphens, and underscores are allowed.',
                'INVALID_CONFIG'
            );
        }
    }
    /**
     * Validate node ID format
     * Figma node IDs are typically in format "123:456"
     */
    private validateNodeId(nodeId: string): void {
        if (!nodeId || typeof nodeId !== 'string') {
            throw new FigmaError('Node ID is required', 'INVALID_CONFIG');
        }
        // Figma node IDs are typically in format "number:number"
        const nodePattern = /^\d+:\d+$/;
        if (!nodePattern.test(nodeId.trim())) {
            throw new FigmaError(
                `Invalid node ID format: "${nodeId}". Expected format: "123:456"`,
                'INVALID_CONFIG'
            );
        }
    }
    // Getters for config values
    get fileId(): string {
        return this.config.fileId;
    }
    get defaultPageName(): string {
        return this.config.defaultPageName;
    }
    get maxDepth(): number {
        return this.config.maxDepth;
    }
    get maxComponents(): number {
        return this.config.maxComponents;
    }
    get commonNodes(): Record<string, string> {
        return {...this.config.commonNodes}; // Return copy to prevent mutation
    }
    /**
     * Get a specific common node ID with validation
     */
    getCommonNode(name: string): string {
        const nodeId = this.config.commonNodes[name];
        if (!nodeId) {
            const availableNodes = Object.keys(this.config.commonNodes).join(', ');
            throw new FigmaError(
                `Common node "${name}" not found. Available nodes: ${availableNodes || 'none'}`,
                'NODE_NOT_FOUND'
            );
        }
        return nodeId;
    }
    /**
     * Create a new config instance with updated values
     * This maintains immutability
     */
    withUpdates(updates: FigmaConfigOptions): FigmaConfigManager {
        return new FigmaConfigManager({
            fileId: updates.fileId || this.config.fileId,
            defaultPageName: updates.defaultPageName || this.config.defaultPageName,
            maxDepth: updates.maxDepth || this.config.maxDepth,
            maxComponents: updates.maxComponents || this.config.maxComponents,
            commonNodes: {...this.config.commonNodes, ...(updates.commonNodes || {})}
        });
    }
    /**
     * Add or update a common node
     */
    withCommonNode(name: string, nodeId: string): FigmaConfigManager {
        this.validateNodeId(nodeId);
        return this.withUpdates({
            commonNodes: {[name]: nodeId}
        });
    }
    /**
     * Get config summary for debugging
     */
    getSummary(): string {
        return `Figma Config:
  File ID: ${this.fileId}
  Default Page: ${this.defaultPageName}
  Max Depth: ${this.maxDepth}
  Max Components: ${this.maxComponents}
  Common Nodes: ${Object.keys(this.commonNodes).length} defined`;
    }
    /**
     * Export config as plain object
     */
    toObject(): FigmaConfig {
        return {...this.config};
    }
}
// Create default instance
export const figmaConfig = new FigmaConfigManager();
// Helper functions for backward compatibility
export function getFileId(): string {
    return figmaConfig.fileId;
}
export function validateFileId(fileId?: string): boolean {
    try {
        new FigmaConfigManager({fileId: fileId || figmaConfig.fileId});
        return true;
    } catch (error) {
        if (error instanceof FigmaError) {
            throw error;
        }
        throw new FigmaError(`Validation failed: ${error}`, 'VALIDATION_ERROR');
    }
}
// Enhanced helper functions
export function validateNodeId(nodeId: string): boolean {
    const config = new FigmaConfigManager();
    try {
        config['validateNodeId'](nodeId); // Access private method for validation
        return true;
    } catch (error) {
        if (error instanceof FigmaError) {
            throw error;
        }
        throw new FigmaError(`Node ID validation failed: ${error}`, 'VALIDATION_ERROR');
    }
}
export function createConfig(options: FigmaConfigOptions): FigmaConfigManager {
    return new FigmaConfigManager(options);
}
// Environment-based config creation
export function createConfigFromEnv(): FigmaConfigManager {
    return new FigmaConfigManager({
        fileId: process.env.FIGMA_FILE_ID,
        maxDepth: process.env.FIGMA_MAX_DEPTH ? parseInt(process.env.FIGMA_MAX_DEPTH, 10) : undefined,
        maxComponents: process.env.FIGMA_MAX_COMPONENTS ? parseInt(process.env.FIGMA_MAX_COMPONENTS, 10) : undefined
    });
}
```
--------------------------------------------------------------------------------
/src/utils/figma-url-parser.ts:
--------------------------------------------------------------------------------
```typescript
// src/utils/figma-url-parser.mts
import {FigmaError} from '../types/errors.js';
/**
 * Component input parsing result
 */
export interface ComponentInput {
    fileId: string;
    nodeId: string;
    source: 'url' | 'direct';
    isValid: boolean;
    error?: string;
}
/**
 * Parse Figma component input from URL or direct parameters
 * Supports:
 * - https://www.figma.com/file/{fileId}/...?node-id={nodeId}
 * - https://www.figma.com/design/{fileId}/...?node-id={nodeId}
 * - Direct fileId and nodeId parameters
 */
export function parseComponentInput(input: string, nodeId?: string): ComponentInput {
    try {
        // If nodeId is provided separately, treat as direct input
        if (nodeId) {
            const validatedFileId = validateFileId(input.trim());
            const validatedNodeId = validateAndConvertNodeId(nodeId.trim());
            return {
                fileId: validatedFileId,
                nodeId: validatedNodeId,
                source: 'direct',
                isValid: true
            };
        }
        // Try to parse as URL first
        if (input.includes('figma.com')) {
            return parseFromUrl(input);
        }
        // Check if it's in fileId:nodeId format
        if (input.includes(':') && input.split(':').length === 2) {
            const [fileIdPart, nodeIdPart] = input.split(':');
            const validatedFileId = validateFileId(fileIdPart.trim());
            const validatedNodeId = validateAndConvertNodeId(`${fileIdPart.trim()}:${nodeIdPart.trim()}`);
            return {
                fileId: validatedFileId,
                nodeId: validatedNodeId,
                source: 'direct',
                isValid: true
            };
        }
        throw new FigmaError('Invalid input format. Expected Figma URL or fileId:nodeId format', 'INVALID_INPUT');
    } catch (error) {
        return {
            fileId: '',
            nodeId: '',
            source: 'direct',
            isValid: false,
            error: error instanceof Error ? error.message : String(error)
        };
    }
}
/**
 * Parse component info from Figma URL
 */
function parseFromUrl(url: string): ComponentInput {
    try {
        const urlObj = new URL(url.trim());
        // Check if it's a valid Figma URL
        if (!urlObj.hostname.includes('figma.com')) {
            throw new FigmaError('Not a valid Figma URL', 'INVALID_URL');
        }
        // Extract file ID from path
        // Paths can be: /file/{fileId}/... or /design/{fileId}/...
        const pathMatch = urlObj.pathname.match(/\/(file|design)\/([a-zA-Z0-9\-_]+)/);
        if (!pathMatch || !pathMatch[2]) {
            throw new FigmaError('Could not extract file ID from URL', 'INVALID_URL');
        }
        const fileId = pathMatch[2];
        const validatedFileId = validateFileId(fileId);
        // Extract node ID from query parameters
        const nodeIdParam = urlObj.searchParams.get('node-id') || urlObj.searchParams.get('node_id');
        if (!nodeIdParam) {
            throw new FigmaError('Node ID not found in URL parameters', 'INVALID_URL');
        }
        // Node ID in URL is often in format "123-456" but API expects "123:456"
        const validatedNodeId = validateAndConvertNodeId(nodeIdParam);
        return {
            fileId: validatedFileId,
            nodeId: validatedNodeId,
            source: 'url',
            isValid: true
        };
    } catch (error) {
        throw new FigmaError(
            `Failed to parse Figma URL: ${error instanceof Error ? error.message : String(error)}`,
            'URL_PARSE_ERROR'
        );
    }
}
/**
 * Validate Figma file ID format
 */
function validateFileId(fileId: string): string {
    if (!fileId || typeof fileId !== 'string') {
        throw new FigmaError('File ID is required', 'INVALID_FILE_ID');
    }
    const trimmed = fileId.trim();
    if (trimmed.length === 0) {
        throw new FigmaError('File ID cannot be empty', 'INVALID_FILE_ID');
    }
    if (trimmed.length < 10 || trimmed.length > 50) {
        throw new FigmaError(
            `Invalid file ID length: ${trimmed.length}. Expected 10-50 characters.`,
            'INVALID_FILE_ID'
        );
    }
    // File IDs contain alphanumeric characters, hyphens, and underscores
    const validPattern = /^[a-zA-Z0-9\-_]+$/;
    if (!validPattern.test(trimmed)) {
        throw new FigmaError(
            'Invalid file ID format. Only alphanumeric characters, hyphens, and underscores allowed.',
            'INVALID_FILE_ID'
        );
    }
    return trimmed;
}
/**
 * Validate and convert node ID to correct format
 * Handles both "123-456" (URL format) and "123:456" (API format)
 */
function validateAndConvertNodeId(nodeId: string): string {
    if (!nodeId || typeof nodeId !== 'string') {
        throw new FigmaError('Node ID is required', 'INVALID_NODE_ID');
    }
    const trimmed = nodeId.trim();
    if (trimmed.length === 0) {
        throw new FigmaError('Node ID cannot be empty', 'INVALID_NODE_ID');
    }
    // Convert URL format (123-456) to API format (123:456) if needed
    let apiFormat = trimmed;
    if (trimmed.includes('-') && !trimmed.includes(':')) {
        // Replace first hyphen with colon (handle cases like "123-456-789")
        const parts = trimmed.split('-');
        if (parts.length >= 2) {
            apiFormat = `${parts[0]}:${parts.slice(1).join('-')}`;
        }
    }
    // Validate API format
    const nodePattern = /^\d+:\d+$/;
    if (!nodePattern.test(apiFormat)) {
        throw new FigmaError(
            `Invalid node ID format: "${trimmed}". Expected format: "123:456" or "123-456"`,
            'INVALID_NODE_ID'
        );
    }
    return apiFormat;
}
/**
 * Check if a string is a valid node ID format
 */
export function isValidNodeIdFormat(nodeId: string): boolean {
    try {
        validateAndConvertNodeId(nodeId);
        return true;
    } catch {
        return false;
    }
}
/**
 * Extract file ID and node ID from various input formats
 * Used for batch operations or validation
 */
export function extractIds(input: string): {fileId?: string; nodeId?: string} {
    try {
        const parsed = parseComponentInput(input);
        if (parsed.isValid) {
            return {
                fileId: parsed.fileId,
                nodeId: parsed.nodeId
            };
        }
    } catch {
        // Ignore errors for this helper function
    }
    return {};
}
/**
 * Generate Figma URL from file ID and node ID
 */
export function generateFigmaUrl(fileId: string, nodeId: string): string {
    const validatedFileId = validateFileId(fileId);
    const validatedNodeId = validateAndConvertNodeId(nodeId);
    // Convert API format back to URL format (123:456 -> 123-456)
    const urlNodeId = validatedNodeId.replace(':', '-');
    return `https://www.figma.com/file/${validatedFileId}?node-id=${urlNodeId}`;
}
/**
 * Check if input looks like a Figma URL
 */
export function isFigmaUrl(input: string): boolean {
    try {
        const url = new URL(input.trim());
        return url.hostname.includes('figma.com') &&
            (url.pathname.includes('/file/') || url.pathname.includes('/design/'));
    } catch {
        return false;
    }
}
```
--------------------------------------------------------------------------------
/src/services/figma.ts:
--------------------------------------------------------------------------------
```typescript
// services/figma.mts
import fetch from 'node-fetch';
import type {FigmaNode, NodeResponse} from '../types/figma.js';
import {
    FigmaError,
    FigmaAuthError,
    FigmaNotFoundError,
    FigmaNetworkError,
    FigmaParseError,
    createFigmaError
} from '../types/errors.js';
import {withRetry} from '../utils/retry.js';
export class FigmaService {
    private accessToken: string;
    private baseUrl = 'https://api.figma.com/v1';
    constructor(accessToken: string) {
        if (!accessToken || accessToken.trim().length === 0) {
            throw new FigmaAuthError('Figma access token is required');
        }
        this.accessToken = accessToken;
    }
    /**
     * Core API request method with retry logic
     */
    private async makeRequest<T>(endpoint: string): Promise<T> {
        return withRetry(async () => {
            const url = `${this.baseUrl}${endpoint}`;
            try {
                console.log(`🔄 Making Figma API request: ${endpoint}`);
                const response = await fetch(url, {
                    headers: {
                        'X-Figma-Token': this.accessToken,
                        'Content-Type': 'application/json'
                    }
                });
                if (!response.ok) {
                    let errorDetails = '';
                    try {
                        const errorBody = await response.text();
                        if (errorBody) {
                            const parsedError = JSON.parse(errorBody);
                            errorDetails = parsedError.message || parsedError.error || errorBody;
                        }
                    } catch {
                        // Ignore parse errors for error body
                    }
                    const error = createFigmaError(response, errorDetails);
                    if (response.status === 404) {
                        throw new FigmaNotFoundError('API endpoint', endpoint);
                    }
                    throw error;
                }
                const data = await response.json() as T;
                console.log(`✅ Successfully fetched: ${endpoint}`);
                return data;
            } catch (error) {
                if (error instanceof FigmaError) {
                    throw error;
                }
                if (error instanceof Error) {
                    if (error.message.includes('ENOTFOUND') || error.message.includes('ECONNREFUSED')) {
                        throw new FigmaNetworkError('Unable to connect to Figma API', error);
                    }
                    if (error.name === 'SyntaxError') {
                        throw new FigmaParseError('Invalid JSON response from Figma API', error);
                    }
                }
                throw new FigmaNetworkError(`Unexpected error: ${error}`, error as Error);
            }
        }, {
            maxAttempts: 3,
            initialDelayMs: 1000,
            maxDelayMs: 10000
        });
    }
    /**
     * Get specific nodes by IDs
     */
    async getNodes(fileId: string, nodeIds: string[]): Promise<Record<string, FigmaNode>> {
        if (!nodeIds || nodeIds.length === 0) {
            throw new FigmaError('At least one node ID is required', 'INVALID_INPUT');
        }
        try {
            const data = await this.makeRequest<any>(`/files/${fileId}/nodes?ids=${nodeIds.join(',')}`);
            const nodes: Record<string, FigmaNode> = {};
            Object.entries(data.nodes || {}).forEach(([nodeId, nodeData]: [string, any]) => {
                if (nodeData?.document) {
                    nodes[nodeId] = nodeData.document;
                }
            });
            return nodes;
        } catch (error) {
            if (error instanceof FigmaError) {
                throw error;
            }
            throw new FigmaError(`Failed to fetch nodes: ${error}`, 'FETCH_ERROR');
        }
    }
    /**
     * Get a single node by ID
     */
    async getNode(fileId: string, nodeId: string): Promise<FigmaNode> {
        if (!nodeId || nodeId.trim().length === 0) {
            throw new FigmaError('Node ID is required', 'INVALID_INPUT');
        }
        try {
            const data = await this.makeRequest<NodeResponse>(`/files/${fileId}/nodes?ids=${nodeId}`);
            if (!data.nodes || !data.nodes[nodeId]) {
                throw new FigmaNotFoundError('Node', nodeId);
            }
            const nodeData = data.nodes[nodeId];
            if (!nodeData.document) {
                throw new FigmaParseError('Invalid node structure received from Figma API', nodeData);
            }
            return nodeData.document;
        } catch (error) {
            if (error instanceof FigmaError) {
                throw error;
            }
            throw new FigmaError(`Failed to fetch node ${nodeId}: ${error}`, 'FETCH_ERROR');
        }
    }
    /**
     * Get image export URLs
     */
    async getImageExportUrls(
        fileId: string,
        nodeIds: string[],
        options: {
            format?: 'png' | 'jpg' | 'svg' | 'pdf';
            scale?: number;
            svgIncludeId?: boolean;
            svgSimplifyStroke?: boolean;
            svgOutlineText?: boolean;
        } = {}
    ): Promise<Record<string, string>> {
        if (!nodeIds || nodeIds.length === 0) {
            return {};
        }
        const params = new URLSearchParams({
            ids: nodeIds.join(','),
            format: options.format || 'png'
        });
        if (options.scale && ['png', 'jpg'].includes(options.format || 'png')) {
            params.append('scale', options.scale.toString());
        }
        if (options.format === 'svg') {
            if (options.svgIncludeId !== undefined) {
                params.append('svg_include_id', options.svgIncludeId.toString());
            }
            if (options.svgSimplifyStroke !== undefined) {
                params.append('svg_simplify_stroke', options.svgSimplifyStroke.toString());
            }
            if (options.svgOutlineText !== undefined) {
                params.append('svg_outline_text', options.svgOutlineText.toString());
            }
        }
        try {
            const response = await this.makeRequest<any>(`/images/${fileId}?${params}`);
            if (response.err) {
                throw new FigmaError(`Image export failed: ${response.err}`, 'EXPORT_ERROR');
            }
            // Filter out null values
            const validImages: Record<string, string> = {};
            Object.entries(response.images || {}).forEach(([nodeId, url]) => {
                if (url && typeof url === 'string') {
                    validImages[nodeId] = url;
                }
            });
            return validImages;
        } catch (error) {
            if (error instanceof FigmaError) {
                throw error;
            }
            throw new FigmaError(`Failed to export images: ${error}`, 'EXPORT_ERROR');
        }
    }
    /**
     * Get image fills used in the file
     */
    async getImageFillUrls(fileId: string): Promise<Record<string, string>> {
        try {
            const response = await this.makeRequest<any>(`/files/${fileId}/images`);
            return response.meta?.images || {};
        } catch (error) {
            if (error instanceof FigmaError) {
                throw error;
            }
            throw new FigmaError(`Failed to fetch image fills: ${error}`, 'FETCH_ERROR');
        }
    }
}
```
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
```typescript
import {config as loadEnv} from "dotenv";
import yargs from "yargs";
import {hideBin} from "yargs/helpers";
import {resolve} from "path";
import {readFileSync} from "fs";
import {fileURLToPath} from "url";
import {dirname, join} from "path";
export interface ServerConfig {
    figmaApiKey?: string;
    outputFormat: "yaml" | "json";
    isStdioMode: boolean;
    isHttpMode: boolean;
    isRemoteMode: boolean;
    httpPort: number;
    configSources: {
        figmaApiKey: "cli" | "env" | "none";
        envFile: "cli" | "default";
        stdio: "cli" | "env" | "default";
        http: "cli" | "env" | "default";
        remote: "cli" | "env" | "default";
        port: "cli" | "env" | "default";
    };
}
function maskApiKey(key: string): string {
    if (!key || key.length <= 4) return "****";
    return `****${key.slice(-4)}`;
}
function getPackageVersion(): string {
    try {
        // Get the directory of the current module
        const __filename = fileURLToPath(import.meta.url);
        const __dirname = dirname(__filename);
        
        // Read package.json from the project root (one level up from src)
        const packageJsonPath = join(__dirname, '..', 'package.json');
        const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
        return packageJson.version || '0.0.1';
    } catch (error) {
        // Fallback to environment variable or default
        return process.env.npm_package_version || '0.0.1';
    }
}
interface CliArgs {
    "figma-api-key"?: string;
    env?: string;
    stdio?: boolean;
    http?: boolean;
    remote?: boolean;
    port?: number;
}
export function getServerConfig(): ServerConfig {
    // Parse command line arguments
    const argv = yargs(hideBin(process.argv))
        .options({
            "figma-api-key": {
                type: "string",
                description: "Your Figma API key (can also be set via FIGMA_API_KEY env var)",
            },
            env: {
                type: "string",
                description: "Path to custom .env file to load environment variables from",
            },
            stdio: {
                type: "boolean",
                description: "Run in stdio mode for MCP client communication",
                default: false,
            },
            http: {
                type: "boolean",
                description: "Run in HTTP mode for local testing",
                default: false,
            },
            remote: {
                type: "boolean",
                description: "Run in remote mode - users provide their own Figma API keys",
                default: false,
            },
            port: {
                type: "number",
                description: "Port number for HTTP server",
                default: 3333,
            },
        })
        .help()
        .version(getPackageVersion())
        .parseSync() as CliArgs;
    // Load environment variables from custom path or default
    let envFilePath: string;
    let envFileSource: "cli" | "default";
    if (argv.env) {
        envFilePath = resolve(argv.env);
        envFileSource = "cli";
    } else {
        envFilePath = resolve(process.cwd(), ".env");
        envFileSource = "default";
    }
    // Load .env file with override if custom path provided
    loadEnv({path: envFilePath, override: !!argv.env});
    const config: ServerConfig = {
        figmaApiKey: undefined,
        outputFormat: "json",
        isStdioMode: false,
        isHttpMode: false,
        isRemoteMode: false,
        httpPort: 3333,
        configSources: {
            figmaApiKey: "none",
            envFile: envFileSource,
            stdio: "default",
            http: "default",
            remote: "default",
            port: "default",
        },
    };
    // Handle FIGMA_API_KEY - Users must provide their own API key
    if (argv["figma-api-key"]) {
        config.figmaApiKey = argv["figma-api-key"];
        config.configSources.figmaApiKey = "cli";
    } else if (process.env.FIGMA_API_KEY) {
        config.figmaApiKey = process.env.FIGMA_API_KEY;
        config.configSources.figmaApiKey = "env";
    }
    // Users can provide API key via CLI args, .env file, or HTTP headers (in remote mode)
    // Handle stdio mode
    if (argv.stdio) {
        config.isStdioMode = true;
        config.configSources.stdio = "cli";
    } else if (process.env.NODE_ENV === "cli") {
        config.isStdioMode = true;
        config.configSources.stdio = "env";
    }
    // Handle HTTP mode
    if (argv.http) {
        config.isHttpMode = true;
        config.configSources.http = "cli";
    } else if (process.env.HTTP_MODE === "true") {
        config.isHttpMode = true;
        config.configSources.http = "env";
    }
    // Handle remote mode
    if (argv.remote) {
        config.isRemoteMode = true;
        config.isHttpMode = true; // Remote mode implies HTTP mode
        config.configSources.remote = "cli";
    } else if (process.env.REMOTE_MODE === "true") {
        config.isRemoteMode = true;
        config.isHttpMode = true; // Remote mode implies HTTP mode
        config.configSources.remote = "env";
    }
    // Handle port configuration
    if (argv.port) {
        config.httpPort = argv.port;
        config.configSources.port = "cli";
    } else if (process.env.HTTP_PORT) {
        config.httpPort = parseInt(process.env.HTTP_PORT, 10);
        config.configSources.port = "env";
    }
    // Validate configuration - Users must provide their own API key for ALL modes
    if (!config.figmaApiKey) {
        console.error("Error: FIGMA_API_KEY is required for all modes.");
        console.error("Please provide your Figma API key via one of these methods:");
        console.error("  1. CLI argument: --figma-api-key=YOUR_API_KEY");
        console.error("  2. Environment variable: FIGMA_API_KEY=YOUR_API_KEY in .env file");
        console.error("");
        console.error("Get your API key from: https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens");
        console.error("");
        if (config.isRemoteMode) {
            console.error("Note: In remote mode, this key serves as a fallback.");
            console.error("Users can still provide their own keys via HTTP headers for isolation.");
        }
        console.error("");
        console.error("Examples:");
        console.error("  npx figma-flutter-mcp --figma-api-key=YOUR_KEY --stdio");
        console.error("  echo 'FIGMA_API_KEY=YOUR_KEY' > .env && npx figma-flutter-mcp --stdio");
        console.error("  npx figma-flutter-mcp --figma-api-key=YOUR_KEY --remote");
        process.exit(1);
    }
    // Log configuration sources (only in non-stdio mode)
    if (!config.isStdioMode) {
        console.log("\nConfiguration:");
        console.log(`- ENV_FILE: ${envFilePath} (source: ${config.configSources.envFile})`);
        if (config.figmaApiKey) {
            console.log(
                `- FIGMA_API_KEY: ${maskApiKey(config.figmaApiKey)} (source: ${config.configSources.figmaApiKey})`
            );
        } else {
            console.log(`- FIGMA_API_KEY: Not set - users will provide their own (source: ${config.configSources.figmaApiKey})`);
        }
        console.log(`- STDIO_MODE: ${config.isStdioMode} (source: ${config.configSources.stdio})`);
        console.log(`- HTTP_MODE: ${config.isHttpMode} (source: ${config.configSources.http})`);
        console.log(`- REMOTE_MODE: ${config.isRemoteMode} (source: ${config.configSources.remote})`);
        if (config.isHttpMode) {
            console.log(`- HTTP_PORT: ${config.httpPort} (source: ${config.configSources.port})`);
        }
        console.log(); // Empty line for better readability
    }
    return config;
}
```
--------------------------------------------------------------------------------
/src/tools/flutter/assets/assets.ts:
--------------------------------------------------------------------------------
```typescript
// tools/flutter/assets.mts
import {z} from "zod";
import type {McpServer} from "@modelcontextprotocol/sdk/server/mcp.js";
import {FigmaService} from "../../../services/figma.js";
import {join} from 'path';
import {
    createAssetsDirectory,
    generateAssetFilename,
    downloadImage,
    getFileStats,
    updatePubspecAssets,
    generateAssetConstants,
    groupAssetsByBaseName,
    type AssetInfo
} from "./asset-manager.js";
export function registerFlutterAssetTools(server: McpServer, figmaApiKey: string) {
    // Tool: Export Flutter Assets
    server.registerTool(
        "export_flutter_assets",
        {
            title: "Export Flutter Assets",
            description: "Export images from Figma nodes and set up Flutter assets directory with pubspec.yaml",
            inputSchema: {
                fileId: z.string().describe("Figma file ID"),
                nodeIds: z.array(z.string()).describe("Array of node IDs to export as images"),
                projectPath: z.string().optional().describe("Path to Flutter project (defaults to current directory)"),
                format: z.enum(['png', 'jpg', 'svg']).default('png').describe("Export format"),
                scale: z.number().optional().default(2).describe("Export scale (1x, 2x, 3x, 4x)"),
                includeMultipleResolutions: z.boolean().optional().default(false).describe("Generate @2x, @3x variants for different screen densities")
            }
        },
        async ({fileId, nodeIds, projectPath = process.cwd(), format = 'png', scale = 2, includeMultipleResolutions = false}) => {
            const token = figmaApiKey;
            if (!token) {
                return {
                    content: [{
                        type: "text",
                        text: "Error: Figma access token not configured."
                    }]
                };
            }
            try {
                const figmaService = new FigmaService(token);
                // First, get node details to filter for actual images/illustrations
                const imageNodes = await filterImageNodes(fileId, nodeIds, figmaService);
                if (imageNodes.length === 0) {
                    return {
                        content: [{
                            type: "text",
                            text: "No image assets found in the specified nodes. Only custom illustrations, photos, and non-icon graphics are exported."
                        }]
                    };
                }
                // Create assets directory structure
                const assetsDir = await createAssetsDirectory(projectPath);
                let downloadedAssets: AssetInfo[] = [];
                // Process each resolution if multi-resolution is enabled
                const scales = includeMultipleResolutions ? [1, 2, 3] : [scale];
                for (const currentScale of scales) {
                    const imageUrls = await figmaService.getImageExportUrls(fileId, imageNodes.map(n => n.id), {
                        format,
                        scale: currentScale
                    });
                    for (const node of imageNodes) {
                        const imageUrl = imageUrls[node.id];
                        if (!imageUrl) continue;
                        const filename = generateAssetFilename(node.name, format, currentScale, includeMultipleResolutions);
                        const filepath = join(assetsDir, filename);
                        // Download the image
                        await downloadImage(imageUrl, filepath);
                        // Get file size for reporting
                        const stats = await getFileStats(filepath);
                        downloadedAssets.push({
                            nodeId: node.id,
                            nodeName: node.name,
                            filename,
                            path: `assets/images/${filename}`,
                            size: stats.size
                        });
                    }
                }
                // Update pubspec.yaml
                const pubspecPath = join(projectPath, 'pubspec.yaml');
                await updatePubspecAssets(pubspecPath, downloadedAssets);
                // Generate asset constants file
                const constantsFile = await generateAssetConstants(downloadedAssets, projectPath);
                let output = `Successfully exported ${imageNodes.length} image assets to Flutter project!\n\n`;
                output += `Assets Directory: ${assetsDir}\n\n`;
                output += `Downloaded Assets:\n`;
                // Group by base name for cleaner output
                const groupedAssets = groupAssetsByBaseName(downloadedAssets);
                Object.entries(groupedAssets).forEach(([baseName, assets]) => {
                    output += `- ${baseName}:\n`;
                    assets.forEach(asset => {
                        output += `  • ${asset.filename} (${asset.size})\n`;
                    });
                });
                output += `\nPubspec Configuration:\n`;
                output += `- Merged asset declarations into pubspec.yaml\n`;
                output += `- Assets available under: assets/images/\n\n`;
                output += `Generated Code:\n`;
                output += `- Merged asset constants into: ${constantsFile}\n`;
                output += `- Import in your Flutter code: import 'package:your_app/constants/assets.dart';\n`;
                return {
                    content: [{type: "text", text: output}]
                };
            } catch (error) {
                return {
                    content: [{
                        type: "text",
                        text: `Error exporting assets: ${error instanceof Error ? error.message : String(error)}`
                    }]
                };
            }
        }
    );
}
// OPTIMIZED: Helper functions for filtering image nodes - only searches within target nodes
async function filterImageNodes(fileId: string, targetNodeIds: string[], figmaService: any): Promise<Array<{id: string, name: string, node: any}>> {
    // OPTIMIZED: Only get the target nodes instead of the entire file (massive performance improvement)
    const targetNodes = await figmaService.getNodes(fileId, targetNodeIds);
    const allNodesWithImages: Array<{id: string, name: string, node: any}> = [];
    function extractImageNodes(node: any, nodeId: string = node.id): void {
        // Check if this node has image fills
        if (node.fills && node.fills.some((fill: any) => fill.type === 'IMAGE' && fill.visible !== false)) {
            allNodesWithImages.push({
                id: nodeId,
                name: node.name,
                node: node
            });
        }
        // Check if this is a vector/illustration that should be exported
        if (node.type === 'VECTOR' && node.name) {
            const name = node.name.toLowerCase();
            if ((name.includes('image') || name.includes('illustration') || name.includes('graphic')) &&
                !name.includes('icon') && !name.includes('button')) {
                allNodesWithImages.push({
                    id: nodeId,
                    name: node.name,
                    node: node
                });
            }
        }
        // Recursively check children
        if (node.children) {
            node.children.forEach((child: any) => {
                extractImageNodes(child, child.id);
            });
        }
    }
    // OPTIMIZED: Extract only from target nodes instead of entire file
    // This eliminates the need for expensive boundary checking since we only search within target nodes
    Object.values(targetNodes).forEach((node: any) => {
        extractImageNodes(node);
    });
    // OPTIMIZED: No filtering needed since we only searched within target nodes
    return allNodesWithImages;
}
// REMOVED: isNodeWithinTarget function no longer needed since we only search within target nodes
function toCamelCase(str: string): string {
    return str
        .toLowerCase()
        .replace(/[^a-z0-9]/g, '_')
        .replace(/_+/g, '_')
        .replace(/^_|_$/g, '')
        .replace(/_(.)/g, (_, char) => char.toUpperCase());
}
```
--------------------------------------------------------------------------------
/src/extractors/typography/core.ts:
--------------------------------------------------------------------------------
```typescript
// src/extractors/typography/core.mts
import type {FigmaNode} from '../../types/figma.js';
import type {
    TypographyStyle,
    TypographyDefinition,
    TypographyExtractionContext,
    TypographyExtractorFn,
    TypographyExtractionOptions
} from './types.js';
import {
    extractTypographyFromThemeFrame,
} from './extractor.js';
/**
 * Main typography extraction orchestrator
 */
export class TypographyExtractor {
    private typographyLibrary: TypographyDefinition[] = [];
    private typographyMap = new Map<string, string>(); // style hash -> ID
    private fontFamilies = new Set<string>();
    /**
     * Extract theme typography from a specific frame
     */
    extractThemeFromFrame(frameNode: FigmaNode): TypographyStyle[] {
        return extractTypographyFromThemeFrame(frameNode);
    }
    /**
     * Extract typography from a single node recursively
     */
    private extractFromNode(
        node: FigmaNode,
        extractors: TypographyExtractorFn[],
        context: TypographyExtractionContext,
        options: TypographyExtractionOptions
    ): void {
        if (context.currentDepth > context.maxDepth) {
            return;
        }
        // Skip hidden nodes unless explicitly included
        if (!options.includeHiddenText && node.visible === false) {
            return;
        }
        // Apply all typography extractors to this node
        extractors.forEach(extractor => {
            extractor(node, context, this.typographyLibrary);
        });
        // Process children
        if (node.children && node.children.length > 0) {
            const childContext = {...context, currentDepth: context.currentDepth + 1};
            node.children.forEach(child => {
                this.extractFromNode(child, extractors, childContext, options);
            });
        }
    }
    /**
     * Add or get existing typography from library
     */
    addTypography(
        fontFamily: string,
        fontSize: number,
        fontWeight: number,
        lineHeight: number,
        letterSpacing: number,
        nodeName: string
    ): string {
        // Create style hash for deduplication
        const styleHash = JSON.stringify({
            fontFamily,
            fontSize,
            fontWeight,
            lineHeight,
            letterSpacing
        });
        // Check if typography already exists
        const existingId = this.typographyMap.get(styleHash);
        if (existingId) {
            // Increment usage count
            const typography = this.typographyLibrary.find(t => t.id === existingId);
            if (typography) {
                typography.usageCount++;
            }
            return existingId;
        }
        // Create new typography definition
        const typographyId = `typography_${this.typographyLibrary.length + 1}`;
        const typographyDef: TypographyDefinition = {
            id: typographyId,
            name: this.generateTypographyName(fontSize, fontWeight, nodeName),
            fontFamily,
            fontSize,
            fontWeight,
            lineHeight,
            letterSpacing,
            usage: this.categorizeTypographyUsage(fontSize, nodeName),
            usageCount: 1,
            dartName: this.generateDartSafeName(fontSize, fontWeight, nodeName)
        };
        this.typographyLibrary.push(typographyDef);
        this.typographyMap.set(styleHash, typographyId);
        this.fontFamilies.add(fontFamily);
        return typographyId;
    }
    /**
     * Get current typography library
     */
    getTypographyLibrary(): TypographyDefinition[] {
        return [...this.typographyLibrary];
    }
    /**
     * Get typography by usage category
     */
    getTypographyByUsage(usage: TypographyDefinition['usage']): TypographyDefinition[] {
        return this.typographyLibrary.filter(typography => typography.usage === usage);
    }
    /**
     * Get most used typography
     */
    getMostUsedTypography(limit: number = 10): TypographyDefinition[] {
        return [...this.typographyLibrary]
            .sort((a, b) => b.usageCount - a.usageCount)
            .slice(0, limit);
    }
    /**
     * Get all font families used
     */
    getFontFamilies(): Set<string> {
        return new Set(this.fontFamilies);
    }
    /**
     * Get primary font family (most used)
     */
    getPrimaryFontFamily(): string | null {
        if (this.fontFamilies.size === 0) return null;
        // Count usage of each font family
        const familyCount = new Map<string, number>();
        this.typographyLibrary.forEach(typography => {
            const count = familyCount.get(typography.fontFamily) || 0;
            familyCount.set(typography.fontFamily, count + typography.usageCount);
        });
        // Find most used font family
        let maxCount = 0;
        let primaryFamily = null;
        familyCount.forEach((count, family) => {
            if (count > maxCount) {
                maxCount = count;
                primaryFamily = family;
            }
        });
        return primaryFamily;
    }
    /**
     * Reset all libraries for new extraction
     */
    private resetLibraries(): void {
        this.typographyLibrary = [];
        this.typographyMap.clear();
        this.fontFamilies.clear();
    }
    /**
     * Generate meaningful typography name
     */
    private generateTypographyName(fontSize: number, fontWeight: number, nodeName: string): string {
        // Try to infer name from node context
        const name = nodeName.toLowerCase();
        if (name.includes('heading') || name.includes('title')) return 'Heading';
        if (name.includes('subtitle')) return 'Subtitle';
        if (name.includes('body')) return 'Body';
        if (name.includes('caption')) return 'Caption';
        if (name.includes('button')) return 'Button';
        if (name.includes('label')) return 'Label';
        // Generate name based on size and weight
        let sizeName = 'Body';
        if (fontSize >= 32) sizeName = 'DisplayLarge';
        else if (fontSize >= 28) sizeName = 'DisplayMedium';
        else if (fontSize >= 24) sizeName = 'DisplaySmall';
        else if (fontSize >= 22) sizeName = 'HeadlineLarge';
        else if (fontSize >= 20) sizeName = 'HeadlineMedium';
        else if (fontSize >= 18) sizeName = 'HeadlineSmall';
        else if (fontSize >= 16) sizeName = 'BodyLarge';
        else if (fontSize >= 14) sizeName = 'BodyMedium';
        else if (fontSize >= 12) sizeName = 'BodySmall';
        else if (fontSize >= 11) sizeName = 'LabelLarge';
        else if (fontSize >= 10) sizeName = 'LabelMedium';
        else sizeName = 'LabelSmall';
        // Add weight if not regular
        if (fontWeight >= 700) {
            sizeName += 'Bold';
        } else if (fontWeight >= 600) {
            sizeName += 'SemiBold';
        } else if (fontWeight >= 500) {
            sizeName += 'Medium';
        } else if (fontWeight <= 300) {
            sizeName += 'Light';
        }
        return sizeName;
    }
    /**
     * Generate Dart-safe property name
     */
    private generateDartSafeName(fontSize: number, fontWeight: number, nodeName: string): string {
        const baseName = this.generateTypographyName(fontSize, fontWeight, nodeName);
        
        // Convert to camelCase and ensure it's Dart-safe
        return baseName.charAt(0).toLowerCase() + baseName.slice(1)
            .replace(/[^a-zA-Z0-9]/g, '')
            .replace(/^\d/, '_$&'); // Prefix with underscore if starts with number
    }
    /**
     * Categorize typography usage
     */
    private categorizeTypographyUsage(fontSize: number, nodeName: string): TypographyDefinition['usage'] {
        const name = nodeName.toLowerCase();
        // Check name patterns first
        if (name.includes('heading') || name.includes('title') || name.includes('h1') || name.includes('h2') || name.includes('h3')) {
            return 'heading';
        }
        if (name.includes('body') || name.includes('paragraph')) return 'body';
        if (name.includes('caption') || name.includes('small')) return 'caption';
        if (name.includes('button')) return 'button';
        if (name.includes('label')) return 'label';
        // Fallback to size-based categorization
        if (fontSize >= 20) return 'heading';
        if (fontSize >= 14) return 'body';
        if (fontSize >= 12) return 'caption';
        return 'other';
    }
}
/**
 * Convenience function to extract theme typography from a frame
 */
export function extractThemeTypography(frameNode: FigmaNode): TypographyStyle[] {
    const extractor = new TypographyExtractor();
    return extractor.extractThemeFromFrame(frameNode);
}
```
--------------------------------------------------------------------------------
/src/tools/flutter/theme/colors/theme-tool.ts:
--------------------------------------------------------------------------------
```typescript
// src/tools/flutter/simple-theme-tool.mts
import {z} from "zod";
import type {McpServer} from "@modelcontextprotocol/sdk/server/mcp.js";
import {FigmaService} from "../../../../services/figma.js";
import {extractThemeColors} from "../../../../extractors/colors/index.js";
import {SimpleThemeGenerator} from "./theme-generator.js";
import {join} from 'path';
export function registerThemeTools(server: McpServer, figmaApiKey: string) {
    server.registerTool(
        "extract_theme_colors",
        {
            title: "Extract Theme Colors from Frame",
            description: "Extract colors from a Figma theme frame containing color swatches with labels",
            inputSchema: {
                fileId: z.string().describe("Figma file ID"),
                nodeId: z.string().describe("Theme frame node ID containing color swatches"),
                projectPath: z.string().optional().describe("Path to Flutter project (defaults to current directory)"),
                generateThemeData: z.boolean().optional().describe("Generate Flutter ThemeData class (defaults to false)")
            }
        },
        async ({fileId, nodeId, projectPath = process.cwd(), generateThemeData = false}) => {
            const token = figmaApiKey;
            if (!token) {
                return {
                    content: [{
                        type: "text",
                        text: "Error: Figma access token not configured."
                    }]
                };
            }
            try {
                // Initialize services
                const figmaService = new FigmaService(token);
                const generator = new SimpleThemeGenerator();
                // Get the specific theme frame node
                const themeFrame = await figmaService.getNode(fileId, nodeId);
                if (!themeFrame) {
                    return {
                        content: [{
                            type: "text",
                            text: `Theme frame with node ID "${nodeId}" not found.`
                        }]
                    };
                }
                // Extract colors from the theme frame
                const themeColors = extractThemeColors(themeFrame);
                if (themeColors.length === 0) {
                    return {
                        content: [{
                            type: "text",
                            text: `No colors found in theme frame "${themeFrame.name}". Make sure the frame contains color swatches with text labels.`
                        }]
                    };
                }
                // Generate AppColors class
                const outputPath = join(projectPath, 'lib', 'theme');
                const generatedFilePath = await generator.generateAppColors(themeColors, outputPath, {
                    generateThemeData,
                    includeColorScheme: true,
                    includeMaterialColors: true
                });
                // Create success report
                let output = `Successfully extracted theme colors!\n\n`;
                output += `Theme Frame: ${themeFrame.name}\n`;
                output += `Node ID: ${nodeId}\n`;
                output += `Colors found: ${themeColors.length}\n`;
                output += `Generated: ${generatedFilePath}\n`;
                if (generateThemeData) {
                    output += `Theme Data: ${join(outputPath, 'app_theme.dart')}\n`;
                }
                output += `\n`;
                output += `Extracted Colors:\n`;
                themeColors.forEach((color, index) => {
                    output += `${index + 1}. ${color.name}: ${color.hex}\n`;
                });
                output += `\nGenerated Files:\n`;
                output += `• app_colors.dart - Color constants\n`;
                if (generateThemeData) {
                    output += `• app_theme.dart - Flutter ThemeData\n`;
                }
                output += `\nUsage Examples:\n`;
                output += `// Colors:\n`;
                output += `Container(color: AppColors.primary)\n`;
                output += `Text('Hello', style: TextStyle(color: AppColors.backgroundDark))\n`;
                
                if (generateThemeData) {
                    output += `\n// Theme:\n`;
                    output += `MaterialApp(\n`;
                    output += `  theme: AppTheme.lightTheme,\n`;
                    output += `  // ... your app\n`;
                    output += `)\n`;
                }
                return {
                    content: [{type: "text", text: output}]
                };
            } catch (error) {
                return {
                    content: [{
                        type: "text",
                        text: `Error extracting theme colors: ${error instanceof Error ? error.message : String(error)}`
                    }]
                };
            }
        }
    );
    // Helper tool to inspect a frame structure
    server.registerTool(
        "inspect_theme_frame",
        {
            title: "Inspect Theme Frame Structure",
            description: "Inspect the structure of a theme frame to understand its contents before extraction",
            inputSchema: {
                fileId: z.string().describe("Figma file ID"),
                nodeId: z.string().describe("Frame node ID to inspect")
            }
        },
        async ({fileId, nodeId}) => {
            const token = figmaApiKey;
            if (!token) {
                return {
                    content: [{
                        type: "text",
                        text: "Error: Figma access token not configured."
                    }]
                };
            }
            try {
                const figmaService = new FigmaService(token);
                const frameNode = await figmaService.getNode(fileId, nodeId);
                if (!frameNode) {
                    return {
                        content: [{
                            type: "text",
                            text: `Frame with node ID "${nodeId}" not found.`
                        }]
                    };
                }
                let output = `Frame Inspection Report\n\n`;
                output += `Frame Name: ${frameNode.name}\n`;
                output += `Frame Type: ${frameNode.type}\n`;
                output += `Node ID: ${nodeId}\n`;
                output += `Children: ${frameNode.children?.length || 0}\n\n`;
                if (frameNode.children && frameNode.children.length > 0) {
                    output += `Frame Contents:\n`;
                    frameNode.children.forEach((child, index) => {
                        output += `${index + 1}. ${child.name} (${child.type})\n`;
                        // Show color if it has one
                        if (child.fills && Array.isArray(child.fills)) {
                            const solidFill = child.fills.find(fill => fill.type === 'SOLID' && fill.color);
                            if (solidFill && solidFill.color) {
                                const hex = rgbaToHex(solidFill.color);
                                output += `   Color: ${hex}\n`;
                            }
                        }
                        // Show text children
                        if (child.children) {
                            const textChildren = child.children.filter(c => c.type === 'TEXT');
                            if (textChildren.length > 0) {
                                output += `   Text: ${textChildren.map(t => t.name).join(', ')}\n`;
                            }
                        }
                    });
                } else {
                    output += `Frame is empty or has no children.\n`;
                }
                output += `\nThis frame ${frameNode.children && frameNode.children.length > 0 ? 'can' : 'cannot'} be used for theme color extraction.\n`;
                return {
                    content: [{type: "text", text: output}]
                };
            } catch (error) {
                return {
                    content: [{
                        type: "text",
                        text: `Error inspecting frame: ${error instanceof Error ? error.message : String(error)}`
                    }]
                };
            }
        }
    );
}
function rgbaToHex(color: {r: number; g: number; b: number; a?: number}): string {
    const r = Math.round(color.r * 255);
    const g = Math.round(color.g * 255);
    const b = Math.round(color.b * 255);
    return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`.toUpperCase();
}
```
--------------------------------------------------------------------------------
/src/tools/flutter/assets/asset-manager.ts:
--------------------------------------------------------------------------------
```typescript
// tools/flutter/asset-manager.mts
import {writeFile, mkdir, readFile} from 'fs/promises';
import {join, dirname} from 'path';
export interface AssetInfo {
    nodeId: string;
    nodeName: string;
    filename: string;
    path: string;
    size: string;
}
export async function createAssetsDirectory(projectPath: string): Promise<string> {
    const assetsDir = join(projectPath, 'assets', 'images');
    await mkdir(assetsDir, {recursive: true});
    return assetsDir;
}
export async function createSvgAssetsDirectory(projectPath: string): Promise<string> {
    const assetsDir = join(projectPath, 'assets', 'svgs');
    await mkdir(assetsDir, {recursive: true});
    return assetsDir;
}
export function generateAssetFilename(nodeName: string, format: string, scale: number, multiRes: boolean): string {
    // Clean the node name for filename
    const cleanName = nodeName
        .toLowerCase()
        .replace(/[^a-z0-9]/g, '_')
        .replace(/_+/g, '_')
        .replace(/^_|_$/g, '');
    if (multiRes && scale > 1) {
        return `${cleanName}@${scale}x.${format}`;
    }
    return `${cleanName}.${format}`;
}
export function generateSvgFilename(nodeName: string): string {
    // Clean the node name for SVG filename
    const cleanName = nodeName
        .toLowerCase()
        .replace(/[^a-z0-9]/g, '_')
        .replace(/_+/g, '_')
        .replace(/^_|_$/g, '');
    return `${cleanName}.svg`;
}
export async function downloadImage(url: string, filepath: string): Promise<void> {
    const response = await fetch(url);
    if (!response.ok) {
        throw new Error(`Failed to download image: ${response.statusText}`);
    }
    const buffer = await response.arrayBuffer();
    await mkdir(dirname(filepath), {recursive: true});
    await writeFile(filepath, Buffer.from(buffer));
}
export async function getFileStats(filepath: string): Promise<{size: string}> {
    try {
        const {size} = await import('fs').then(fs => fs.promises.stat(filepath));
        return {
            size: size > 1024 * 1024
                ? `${(size / 1024 / 1024).toFixed(1)}MB`
                : `${Math.round(size / 1024)}KB`
        };
    } catch {
        return {size: 'Unknown'};
    }
}
export async function updatePubspecAssets(pubspecPath: string, assets: Array<{path: string}>): Promise<void> {
    let pubspecContent: string;
    try {
        pubspecContent = await readFile(pubspecPath, 'utf-8');
    } catch {
        // If pubspec doesn't exist, create a basic one
        pubspecContent = `name: flutter_app
description: A Flutter application
version: 1.0.0+1
environment:
  sdk: '>=3.0.0 <4.0.0'
dependencies:
  flutter:
    sdk: flutter
dev_dependencies:
  flutter_test:
    sdk: flutter
flutter:
  uses-material-design: true
`;
    }
    // Extract existing assets from pubspec
    const existingAssets = new Set<string>();
    const assetMatch = pubspecContent.match(/assets:\s*\n((?:    - .*\n)*)/);
    if (assetMatch) {
        const existingAssetLines = assetMatch[1].match(/    - .*/g) || [];
        existingAssetLines.forEach(line => {
            const assetPath = line.replace(/^\s*-\s*/, '').trim();
            existingAssets.add(assetPath);
        });
    }
    // Add new assets to existing ones
    const newAssetPaths = assets.map(a => a.path);
    newAssetPaths.forEach(path => existingAssets.add(path));
    // Convert back to formatted lines
    const allAssetPaths = Array.from(existingAssets).sort().map(path => `    - ${path}`);
    if (pubspecContent.includes('assets:')) {
        // Replace existing assets section with merged assets
        pubspecContent = pubspecContent.replace(
            /assets:\s*\n(?:    - .*\n)*/,
            `assets:\n${allAssetPaths.join('\n')}\n`
        );
    } else if (pubspecContent.includes('flutter:')) {
        // Add assets to existing flutter section
        pubspecContent = pubspecContent.replace(
            'flutter:',
            `flutter:\n  assets:\n${allAssetPaths.join('\n')}`
        );
    } else {
        // Add flutter section with assets
        pubspecContent += `\nflutter:\n  assets:\n${allAssetPaths.join('\n')}\n`;
    }
    await writeFile(pubspecPath, pubspecContent);
}
export async function generateAssetConstants(assets: Array<{filename: string, nodeName: string}>, projectPath: string): Promise<string> {
    const constantsDir = join(projectPath, 'lib', 'constants');
    await mkdir(constantsDir, {recursive: true});
    const constantsPath = join(constantsDir, 'assets.dart');
    // Read existing constants if they exist
    const existingConstants = new Map<string, string>();
    try {
        const existingContent = await readFile(constantsPath, 'utf-8');
        // Extract existing constants using regex
        const constantMatches = existingContent.matchAll(/static const String (\w+) = '([^']+)';/g);
        for (const match of constantMatches) {
            existingConstants.set(match[1], match[2]);
        }
    } catch {
        // File doesn't exist, that's fine
    }
    // Generate unique asset names from new assets
    const uniqueAssets = assets.reduce((acc, asset) => {
        const baseName = asset.filename.replace(/@\d+x/, '').replace(/\.[^.]+$/, '');
        if (!acc[baseName]) {
            acc[baseName] = asset;
        }
        return acc;
    }, {} as Record<string, any>);
    // Add new constants to existing ones
    Object.entries(uniqueAssets).forEach(([baseName, asset]) => {
        const constantName = toCamelCase(asset.nodeName);
        const assetPath = `assets/images/${baseName}.png`; // Use base resolution
        existingConstants.set(constantName, assetPath);
    });
    // Generate the complete constants file
    let constantsContent = `// Generated asset constants\n// Do not edit manually\n\nclass Assets {\n`;
    // Sort constants alphabetically for consistency
    const sortedConstants = Array.from(existingConstants.entries()).sort(([a], [b]) => a.localeCompare(b));
    sortedConstants.forEach(([constantName, assetPath]) => {
        constantsContent += `  static const String ${constantName} = '${assetPath}';\n`;
    });
    constantsContent += `}\n`;
    await writeFile(constantsPath, constantsContent);
    return constantsPath;
}
export async function generateSvgAssetConstants(assets: Array<{filename: string, nodeName: string}>, projectPath: string): Promise<string> {
    const constantsDir = join(projectPath, 'lib', 'constants');
    await mkdir(constantsDir, {recursive: true});
    const constantsPath = join(constantsDir, 'svg_assets.dart');
    // Read existing SVG constants if they exist
    const existingConstants = new Map<string, string>();
    try {
        const existingContent = await readFile(constantsPath, 'utf-8');
        // Extract existing constants using regex
        const constantMatches = existingContent.matchAll(/static const String (\w+) = '([^']+)';/g);
        for (const match of constantMatches) {
            existingConstants.set(match[1], match[2]);
        }
    } catch {
        // File doesn't exist, that's fine
    }
    // Generate unique SVG asset names from new assets
    const uniqueAssets = assets.reduce((acc, asset) => {
        const baseName = asset.filename.replace(/\.svg$/, '');
        if (!acc[baseName]) {
            acc[baseName] = asset;
        }
        return acc;
    }, {} as Record<string, any>);
    // Add new constants to existing ones
    Object.entries(uniqueAssets).forEach(([baseName, asset]) => {
        const constantName = toCamelCase(asset.nodeName);
        const assetPath = `assets/svgs/${baseName}.svg`;
        existingConstants.set(constantName, assetPath);
    });
    // Generate the complete SVG constants file
    let constantsContent = `// Generated SVG asset constants\n// Do not edit manually\n\nclass SvgAssets {\n`;
    // Sort constants alphabetically for consistency
    const sortedConstants = Array.from(existingConstants.entries()).sort(([a], [b]) => a.localeCompare(b));
    sortedConstants.forEach(([constantName, assetPath]) => {
        constantsContent += `  static const String ${constantName} = '${assetPath}';\n`;
    });
    constantsContent += `}\n`;
    await writeFile(constantsPath, constantsContent);
    return constantsPath;
}
export function groupAssetsByBaseName(assets: Array<{filename: string, nodeName: string, size: string}>): Record<string, Array<{filename: string, size: string}>> {
    return assets.reduce((acc, asset) => {
        const baseName = asset.filename.replace(/@\d+x/, '').replace(/\.[^.]+$/, '');
        if (!acc[baseName]) {
            acc[baseName] = [];
        }
        acc[baseName].push({
            filename: asset.filename,
            size: asset.size
        });
        return acc;
    }, {} as Record<string, Array<{filename: string, size: string}>>);
}
function toCamelCase(str: string): string {
    return str
        .toLowerCase()
        .replace(/[^a-z0-9]/g, '_')
        .replace(/_+/g, '_')
        .replace(/^_|_$/g, '')
        .replace(/_(.)/g, (_, char) => char.toUpperCase());
}
```
--------------------------------------------------------------------------------
/docs/figma-framework-mcp.md:
--------------------------------------------------------------------------------
```markdown
## Figma-Framework MCP
Since [Figma Context MCP](https://github.com/gLips/Figma-Context-MCP/) is framework‑agnostic, it does not output code or artifacts tailored to React, Angular, Vue, Flutter, etc. This repository adds a concrete implementation for Flutter (see `docs/figma-flutter-mcp.md`) and documents how to adapt the same architecture to any other framework.
### What you get out of the box
- **Extractors (framework‑agnostic):** Parse Figma nodes into consistent, rich, typed models for components, screens, colors, and typography.
- **Framework tools (Flutter today):** Wrap extractors, add framework‑specific heuristics, asset export, guidance, and code generation.
- **CLI/MCP entry points:** Commands and tools to analyze Figma nodes and generate artifacts into a project.
## Architecture overview
### 1) Extractors (framework‑agnostic core)
Extractors live under `src/extractors/` and work the same regardless of the target framework. They are responsible for traversing Figma nodes and producing structured analysis results that downstream tools can consume.
- **Components:** `src/extractors/components/`
  - Entry points: `analyzeComponent`, `analyzeComponentWithVariants`
  - Outputs `ComponentAnalysis` with metadata, layout, styling, children, nested components, and variants when applicable.
- **Screens:** `src/extractors/screens/`
  - Entry point: `analyzeScreen`
  - Outputs `ScreenAnalysis` with sections (header/content/footer/navigation), assets, and nested components.
- **Colors:** `src/extractors/colors/`
  - Entry points: `extractThemeColors`, `extractColorsFromThemeFrame`
  - Outputs normalized theme color swatches.
- **Typography:** `src/extractors/typography/`
  - Entry point: `extractThemeTypography`
  - Outputs normalized text styles with font family, size, weight, and line height.
These modules use Figma API types (e.g., `FigmaNode`, `FigmaColor`, `FigmaEffect`) and heuristics (e.g., `node.type === 'TEXT' | 'FRAME' | 'COMPONENT'`) to create consistent data across frameworks. The output types are intentionally neutral so they can be mapped to any target tech stack.
> ⚠️ You can add more in the list if the targeted framework needs it.
### 2) Framework tools (target‑specific glue)
Tools live under `src/tools/<framework>/` and are responsible for turning extractor outputs into actionable, framework‑specific results (code, guidance, assets, configuration). For Flutter these are under `src/tools/flutter/`.
Tools typically do the following:
- **Orchestrate extractors** (call `analyzeComponent`, `analyzeScreen`, etc.).
- **Apply classification** (e.g., treat `COMPONENT`/`INSTANCE` as reusable widgets; treat `FRAME` as a screen).
- **Map design semantics** to framework widgets/components (containers, text, icons, layout primitives).
- **Generate code or guidance** (e.g., Flutter widget tree suggestions in `src/tools/flutter/components/helpers.mts`).
- **Export assets** and update project manifests (images, SVGs, `pubspec.yaml` for Flutter).
- **Integrate theming** by referencing color schemes and text themes rather than hardcoding styles when possible.
In other words, extractors give you high‑quality design insights; tools translate those insights into framework‑specific outputs.
## 📝 Porting to a new framework (React, React Native, Angular, Vue)
The fastest path is to keep the extractors intact and replace the Flutter‑specific tools with your framework’s equivalents.
### Folder layout
- Create `src/tools/react/` (or `react-native`, `angular`, `vue`).
- Add submodules as needed, mirroring the Flutter structure:
  - `components/` (analyze and generate component code)
  - `screens/` (analyze frames as screens/pages)
  - `assets/` (export images/SVGs and manage paths)
  - `theme/` (map colors/typography to your framework’s theming system)
  - `helpers.mts` (framework‑specific guidance and code snippets)
### Minimal tool set to implement
- A tool to analyze a component or component set (wraps `analyzeComponent` and optionally variant handling).
- A tool to analyze a full screen (wraps `analyzeScreen`).
- Optional generators to emit framework code:
  - React: `.tsx` components in `src/components/`, pages in `src/pages/` or routes.
  - React Native: `.tsx` components using `View`, `Text`, `Image`, `StyleSheet`.
  - Angular: `.ts/.html/.scss` with schematics for components and modules.
  - Vue: `.vue` Single File Components with `<template>`, `<script>`, and `<style>`.
### Mapping guidelines (how to translate extractor output)
- **Layout**
  - Auto‑layout (direction + spacing) → React/React Native: `display: flex; flexDirection: row|column; gap/margins`; Angular/Vue: templates + CSS.
  - Absolute/stacked → React/React Native: `position: 'absolute'`/wrapper; Angular/Vue: positioned containers.
- **Styling**
  - Fills, strokes, corner radii, shadows → map to CSS/CSS‑in‑JS/StyleSheet equivalents.
  - Effects (`DROP_SHADOW`, `INNER_SHADOW`, blurs) → CSS `box-shadow`, `filter`, or platform‑specific fallbacks.
- **Text**
  - Use `textInfo` to choose the right semantic component (e.g., `h1/h2`, `Button`, `Link`) or apply a `Text` with style.
- **Components vs screens**
  - Treat `COMPONENT`/`INSTANCE` as reusable components.
  - Treat `FRAME` as a screen/page (with a child‑count threshold if you adopt the same heuristic as Flutter tools).
- **Theming**
  - Reference theme tokens (colors/typography) instead of hardcoding styles. For React, prefer ThemeProvider (e.g., MUI, styled‑components, or your design system).
- **Assets**
  - Export images/SVGs and write importable paths using your framework’s conventions (e.g., `public/` for Vite/Next.js, Android/iOS asset catalogs for React Native).
### Example: replacing Flutter guidance with React guidance
Flutter’s helper (`src/tools/flutter/components/helpers.mts`) produces guidance like "Use Row/Column" or `BoxDecoration` for containers. For React, your equivalent helper would:
- Recommend `div`/`section` structure with `display: flex` and `flexDirection`.
- Map paddings/margins/spacing to CSS or a CSS‑in‑JS solution.
- Suggest semantic elements for headings/links/buttons.
- Show small code snippets using your preferred stack (e.g., React + CSS Modules or styled‑components).
Example guidance output snippet (React):
```tsx
// Container layout
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, padding: 16 }}>
  {/* ...children */}
</div>
```
### Code generation (optional but recommended)
Just like the Flutter tools can emit widgets, you can add a lightweight generator to create files and wire imports:
- **React**
  - Components → `src/components/<PascalName>/<PascalName>.tsx`
  - Screens/Pages → `src/pages/<kebab-name>.tsx` (or Next.js routes)
  - Styles → collocate via CSS Modules, styled‑components, Tailwind, etc.
- **React Native**
  - Components → `src/components/<PascalName>.tsx`
  - Use `StyleSheet.create({...})` for styles. Map shadows/radius/fills accordingly.
- **Angular**
  - Use schematics to scaffold `component.ts/html/scss` and update module declarations.
- **Vue**
  - Generate `.vue` SFCs with `<template>` mapped from layout and `<style>` from styling info.
### Assets and theme integration
- Reuse color/typography extractors to populate your theme tokens.
- Export image/SVG assets and centralize paths/constants for your framework.
- Keep asset policies explicit (e.g., which scales to export, where to write constants) and append new entries rather than overwriting existing ones.
### Suggested workflow to add a new framework
1. Create `src/tools/<framework>/` with `components/`, `screens/`, `assets/`, `helpers.mts`.
2. Wrap extractors (`analyzeComponent`, `analyzeScreen`) and print a minimal analysis report.
3. Add guidance helpers that output idiomatic snippets for your framework.
4. Add asset export and theme token mapping.
5. (Optional) Add code generation and a small registry to prevent duplicate components.
6. Register your tools in `src/tools/index.mts` so they’re available via MCP/CLI.
7. Update docs with usage examples and flags.
## 🤨 FAQs
### What is the role of extractors?
Extractors are the framework‑agnostic heart of the system. They convert Figma data into structured, typed models (components, screens, colors, typography) that downstream tools can map to any framework without re‑implementing Figma traversal.
### What is the role of tools?
Tools are the framework‑specific bridge. They orchestrate extractors, apply heuristics (e.g., classify component vs screen), generate guidance or code, export assets, and integrate with a project’s conventions (file layout, theming, configuration).
### I’m building for React. What do I change?
- Do not modify extractors.
- Create `src/tools/react/` with your own helpers and generators.
- Replace Flutter‑specific guidance with React guidance (flexbox, semantic HTML, CSS‑in‑JS, etc.).
- Implement asset export paths and theme references that match your React app setup.
If you’d like a concrete Flutter reference, see `src/tools/flutter/components/helpers.mts` and `src/tools/flutter/screens/screen-tool.mts` and mirror their responsibilities for your framework.
---
If you fork this repository to support another framework (React, React Native, Angular, Vue), please keep it open source so others can build on top of the extractor core and share improvements to the tooling layers.
```
--------------------------------------------------------------------------------
/src/extractors/colors/extractor.ts:
--------------------------------------------------------------------------------
```typescript
// src/extractors/colors/extractor.mts
import type {FigmaNode} from '../../types/figma.js';
import type {
    ThemeColor,
    ColorDefinition,
    ColorExtractorFn
} from './types.js';
/**
 * Extract colors from a theme frame containing color swatches with text labels
 */
export function extractColorsFromThemeFrame(frameNode: FigmaNode): ThemeColor[] {
    const colors: ThemeColor[] = [];
    // Find all child nodes in the frame
    if (!frameNode.children) {
        return colors;
    }
    // Look for color swatches (rectangles/frames) with text labels
    frameNode.children.forEach(child => {
        const colorInfo = extractColorFromNode(child);
        if (colorInfo) {
            colors.push(colorInfo);
        }
    });
    return colors;
}
/**
 * Extract color from a single node (used by theme frame extraction)
 */
function extractColorFromNode(node: FigmaNode): ThemeColor | null {
    // Skip typography-related nodes
    if (isTypographyNode(node)) {
        return null;
    }
    // Check if this node has a solid fill (color swatch)
    const colorHex = getNodeColor(node);
    if (!colorHex) {
        return null;
    }
    // Get the color name from the node name or any text children
    const colorName = getColorName(node);
    if (!colorName) {
        return null;
    }
    return {
        name: colorName,
        hex: colorHex,
        nodeId: node.id
    };
}
/**
 * Get color from node fills or child nodes
 */
function getNodeColor(node: FigmaNode): string | null {
    // Check node fills for solid color
    if (node.fills && Array.isArray(node.fills)) {
        for (const fill of node.fills) {
            if (fill.type === 'SOLID' && fill.color && fill.visible !== false) {
                return rgbaToHex(fill.color);
            }
        }
    }
    // Check children for color rectangles/frames
    if (node.children) {
        for (const child of node.children) {
            if (child.type === 'RECTANGLE' || child.type === 'FRAME') {
                const childColor = getNodeColor(child);
                if (childColor) {
                    return childColor;
                }
            }
        }
    }
    return null;
}
/**
 * Get color name from node or text children
 */
function getColorName(node: FigmaNode): string | null {
    // First, try to get name from text children
    const textName = findTextInNode(node);
    if (textName) {
        return cleanColorName(textName);
    }
    // Fallback to node name if it looks like a color name
    if (node.name && isColorName(node.name)) {
        return cleanColorName(node.name);
    }
    return null;
}
/**
 * Find text content in node or children
 */
function findTextInNode(node: FigmaNode): string | null {
    // Check if this node is text
    if (node.type === 'TEXT' && node.name) {
        return node.name;
    }
    // Check children for text nodes
    if (node.children) {
        for (const child of node.children) {
            if (child.type === 'TEXT' && child.name) {
                return child.name;
            }
            // Recursively check nested children
            const nestedText = findTextInNode(child);
            if (nestedText) {
                return nestedText;
            }
        }
    }
    return null;
}
/**
 * Check if name looks like a color name
 */
function isColorName(name: string): boolean {
    const colorKeywords = [
        'primary', 'secondary', 'accent', 'background', 'surface',
        'text', 'foreground', 'danger', 'error', 'success', 'warning',
        'info', 'light', 'dark', 'grey', 'gray', 'white', 'black'
    ];
    const lowerName = name.toLowerCase();
    return colorKeywords.some(keyword => lowerName.includes(keyword));
}
/**
 * Check if node is typography-related and should be excluded from color extraction
 */
export function isTypographyNode(node: FigmaNode): boolean {
    const typographyKeywords = [
        // Typography size variations
        'heading1', 'heading2', 'heading3', 'heading4', 'heading5', 'heading6',
        'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
        'title1', 'title2', 'title3',
        'subtitle1', 'subtitle2', 'subtitle3',
        'body1', 'body2', 'body3',
        'caption1', 'caption2', 'caption3',
        'label1', 'label2', 'label3',
        'overline1', 'overline2', 'overline3',
        
        // Typography style variations
        'headline', 'subheading', 'subhead',
        'display', 'title', 'subtitle', 'body', 'caption', 'overline', 'label',
        'text-lg', 'text-md', 'text-sm', 'text-xs',
        'large', 'medium', 'small', 'extra-small',
        
        // Typography weight variations
        'regular', 'medium', 'semibold', 'bold', 'light', 'thin',
        'normal', 'heavy', 'black',
        
        // Common typography examples
        'paragraph', 'lorem', 'ipsum', 'sample text', 'example text',
        'placeholder', 'demo text', 'text example',
        
        // Font-related terms
        'font', 'typeface', 'typography', 'text style'
    ];
    const nodeName = node.name.toLowerCase().replace(/\s+/g, '');
    
    // Check node name against typography keywords
    const isTypographyByName = typographyKeywords.some(keyword => 
        nodeName.includes(keyword.toLowerCase().replace(/\s+/g, ''))
    );
    
    // Check if node contains only text children (likely a typography example)
    const isTextOnlyNode = !!(node.children && node.children.length > 0 && 
        node.children.every(child => child.type === 'TEXT'));
    
    // Check if parent or grandparent has typography-related names
    const hasTypographyContext = checkTypographyContext(node, typographyKeywords);
    
    return isTypographyByName || (isTextOnlyNode && hasTypographyContext);
}
/**
 * Check if node has typography context by examining patterns
 */
function checkTypographyContext(node: FigmaNode, typographyKeywords: string[]): boolean {
    // Look for patterns like "Text Style", "Typography", "Font Examples", etc.
    const contextKeywords = [
        'text style', 'typography', 'font example', 'text example',
        'style guide', 'type scale', 'font scale', 'text scale'
    ];
    
    const nodeName = node.name.toLowerCase().replace(/\s+/g, '');
    return contextKeywords.some(keyword => 
        nodeName.includes(keyword.toLowerCase().replace(/\s+/g, ''))
    );
}
/**
 * Clean up color name for use as identifier
 */
function cleanColorName(name: string): string {
    // Remove common prefixes and clean up the name
    return name
        .replace(/^(color|Color)[\s\-_]*/i, '')
        .replace(/[\s\-_]*(color|Color)$/i, '')
        .trim()
        .replace(/\s+/g, ' ')
        .split(' ')
        .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
        .join('');
}
/**
 * Convert RGBA color to hex string
 */
function rgbaToHex(color: {r: number; g: number; b: number; a?: number}): string {
    const r = Math.round(color.r * 255);
    const g = Math.round(color.g * 255);
    const b = Math.round(color.b * 255);
    return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`.toUpperCase();
}
/**
 * Add color to library with deduplication
 */
function addColorToLibrary(
    colorValue: string,
    nodeName: string,
    colorLibrary: ColorDefinition[],
    colorMap: Map<string, string>
): string {
    // Check if color already exists
    const existingId = colorMap.get(colorValue);
    if (existingId) {
        // Increment usage count
        const color = colorLibrary.find(c => c.id === existingId);
        if (color) {
            color.usageCount++;
        }
        return existingId;
    }
    // Create new color definition
    const colorId = `color_${colorLibrary.length + 1}`;
    const colorDef: ColorDefinition = {
        id: colorId,
        name: generateColorName(colorValue, nodeName),
        value: colorValue,
        usage: categorizeColorUsage(nodeName),
        usageCount: 1
    };
    colorLibrary.push(colorDef);
    colorMap.set(colorValue, colorId);
    return colorId;
}
/**
 * Generate meaningful color name from hex value and context
 */
function generateColorName(colorValue: string, nodeName: string): string {
    // Try to infer name from node context
    const name = nodeName.toLowerCase();
    if (name.includes('primary')) return 'Primary';
    if (name.includes('secondary')) return 'Secondary';
    if (name.includes('background')) return 'Background';
    if (name.includes('text')) return 'Text';
    if (name.includes('accent')) return 'Accent';
    // Generate name based on color value
    const colorNames: Record<string, string> = {
        '#ffffff': 'White',
        '#000000': 'Black',
        '#ff0000': 'Red',
        '#00ff00': 'Green',
        '#0000ff': 'Blue',
    };
    return colorNames[colorValue.toLowerCase()] || `Color${Math.random().toString(36).substr(2, 4)}`;
}
/**
 * Categorize color usage based on node name
 */
function categorizeColorUsage(nodeName: string): ColorDefinition['usage'] {
    const name = nodeName.toLowerCase();
    if (name.includes('primary')) return 'primary';
    if (name.includes('secondary')) return 'secondary';
    if (name.includes('background') || name.includes('bg')) return 'background';
    if (name.includes('text') || name.includes('label')) return 'text';
    if (name.includes('accent') || name.includes('highlight')) return 'accent';
    return 'other';
}
```
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
```typescript
import { randomUUID } from "node:crypto";
import express, { type Request, type Response } from "express";
import { Server } from "http";
import cors from "cors";
import {McpServer} from "@modelcontextprotocol/sdk/server/mcp.js";
import {StdioServerTransport} from "@modelcontextprotocol/sdk/server/stdio.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
import {registerAllTools} from "./tools/index.js";
import { Logger } from "./utils/logger.js";
export function createServer(figmaApiKey: string) {
    const server = new McpServer({
        name: "figma-flutter-mcp",
        version: process.env.npm_package_version || "0.0.1"
    });
    registerAllTools(server, figmaApiKey);
    return server;
}
// Create a server instance that can handle per-user API keys
export function createServerForUser(figmaApiKey: string) {
    return createServer(figmaApiKey);
}
let httpServer: Server | null = null;
const transports = {
  streamable: {} as Record<string, StreamableHTTPServerTransport>,
};
// Store MCP server instances per session (for per-user API keys)
const sessionServers = {} as Record<string, McpServer>;
// Helper function to extract Figma API key from request
function extractFigmaApiKey(req: Request, fallbackApiKey?: string): string | null {
  // Try to get from Authorization header (Bearer token)
  const authHeader = req.headers.authorization;
  if (authHeader && authHeader.startsWith('Bearer ')) {
    return authHeader.substring(7);
  }
  
  // Try to get from custom header
  const figmaApiKey = req.headers['x-figma-api-key'] as string;
  if (figmaApiKey) {
    return figmaApiKey;
  }
  
  // Try to get from query parameter (less secure, but convenient for testing)
  const queryApiKey = req.query.figmaApiKey as string;
  if (queryApiKey) { 
    return queryApiKey;
  }
  
  // Fall back to server-wide API key (only for non-remote HTTP mode)
  return fallbackApiKey || null;
}
export async function startMcpServer(figmaApiKey: string): Promise<void> {
    try {
        const server = createServer(figmaApiKey);
        const transport = new StdioServerTransport();
        await server.connect(transport);
        console.error("Figma-to-Flutter MCP Server connected via stdio");
    } catch (error) {
        console.error("Failed to start MCP server:", error);
        process.exit(1);
    }
}
export async function startHttpServer(port: number, figmaApiKey?: string): Promise<void> {
  // For remote mode, we don't create a single server instance
  // Instead, we create per-user servers based on their API keys
  // For non-remote HTTP mode, we use the provided API key
  const app = express();
  // Configure CORS to expose Mcp-Session-Id header for browser-based clients
  app.use(cors({
    origin: '*', // Allow all origins - adjust as needed for production
    exposedHeaders: ['Mcp-Session-Id']
  }));
  // Parse JSON requests for the Streamable HTTP endpoint only, will break SSE endpoint
  app.use("/mcp", express.json());
  // Modern Streamable HTTP endpoint
  app.post("/mcp", async (req, res) => {
    Logger.log("Received StreamableHTTP request");
    const sessionId = req.headers["mcp-session-id"] as string | undefined;
    
    // Extract Figma API key from request
    const userFigmaApiKey = extractFigmaApiKey(req, figmaApiKey);
    if (!userFigmaApiKey) {
      res.status(401).json({
        jsonrpc: "2.0",
        error: {
          code: -32001,
          message: "Unauthorized: Figma API key required. You must provide your own Figma API key via Authorization header (Bearer token), X-Figma-Api-Key header, or figmaApiKey query parameter. Get your API key from: https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens",
        },
        id: null,
      });
      return;
    }
    
    let transport: StreamableHTTPServerTransport;
    let mcpServer: McpServer;
    if (sessionId && transports.streamable[sessionId]) {
      // Reuse existing transport and server
      Logger.log("Reusing existing StreamableHTTP transport for sessionId", sessionId);
      transport = transports.streamable[sessionId];
      mcpServer = sessionServers[sessionId];
    } else if (isInitializeRequest(req.body)) {
      Logger.log("New initialization request for StreamableHTTP");
      
      // Create new server instance for this user's API key
      mcpServer = createServerForUser(userFigmaApiKey);
      
      transport = new StreamableHTTPServerTransport({
        sessionIdGenerator: () => randomUUID(),
        enableJsonResponse: true, // Enable JSON response mode for better remote compatibility
        onsessioninitialized: (newSessionId) => {
          // Store the transport and server by session ID
          transports.streamable[newSessionId] = transport;
          sessionServers[newSessionId] = mcpServer;
          Logger.log("Session initialized with ID:", newSessionId);
        },
      });
      transport.onclose = () => {
        if (transport.sessionId) {
          delete transports.streamable[transport.sessionId];
          delete sessionServers[transport.sessionId];
        }
      };
      await mcpServer.connect(transport);
    } else if (sessionId) {
      // Session ID provided but transport not found - create new one
      Logger.log("Creating new transport for existing sessionId", sessionId);
      
      // Create new server instance for this user's API key
      mcpServer = createServerForUser(userFigmaApiKey);
      
      transport = new StreamableHTTPServerTransport({
        sessionIdGenerator: () => sessionId,
        enableJsonResponse: true, // Enable JSON response mode for better remote compatibility
        onsessioninitialized: (newSessionId) => {
          transports.streamable[newSessionId] = transport;
          sessionServers[newSessionId] = mcpServer;
        },
      });
      transport.onclose = () => {
        if (transport.sessionId) {
          delete transports.streamable[transport.sessionId];
          delete sessionServers[transport.sessionId];
        }
      };
      await mcpServer.connect(transport);
    } else {
      // Invalid request
      Logger.log("Invalid request:", req.body);
      res.status(400).json({
        jsonrpc: "2.0",
        error: {
          code: -32000,
          message: "Bad Request: No valid session ID provided",
        },
        id: null,
      });
      return;
    }
    let progressInterval: NodeJS.Timeout | null = null;
    const progressToken = req.body.params?._meta?.progressToken;
    let progress = 0;
    if (progressToken) {
      Logger.log(
        `Setting up progress notifications for token ${progressToken} on session ${sessionId}`,
      );
      progressInterval = setInterval(async () => {
        Logger.log("Sending progress notification", progress);
        await mcpServer.server.notification({
          method: "notifications/progress",
          params: {
            progress,
            progressToken,
          },
        });
        progress++;
      }, 1000);
    }
    Logger.log("Handling StreamableHTTP request");
    await transport.handleRequest(req, res, req.body);
    if (progressInterval) {
      clearInterval(progressInterval);
    }
    Logger.log("StreamableHTTP request handled");
  });
  // Handle GET requests for SSE streams (using built-in support from StreamableHTTP)
  const handleSessionRequest = async (req: Request, res: Response) => {
    const sessionId = req.headers["mcp-session-id"] as string | undefined;
    if (!sessionId || !transports.streamable[sessionId]) {
      res.status(400).send("Invalid or missing session ID");
      return;
    }
    console.log(`Received session termination request for session ${sessionId}`);
    try {
      const transport = transports.streamable[sessionId];
      await transport.handleRequest(req, res);
    } catch (error) {
      console.error("Error handling session termination:", error);
      if (!res.headersSent) {
        res.status(500).send("Error processing session termination");
      }
    }
  };
  // Handle GET requests for server-to-client notifications via SSE
  app.get("/mcp", handleSessionRequest);
  // Handle DELETE requests for session termination
  app.delete("/mcp", handleSessionRequest);
  httpServer = app.listen(port, () => {
    Logger.log(`HTTP server listening on port ${port}`);
    Logger.log(`StreamableHTTP endpoint available at http://localhost:${port}/mcp`);
  });
  process.on("SIGINT", async () => {
    Logger.log("Shutting down server...");
    // Close all active transports to properly clean up resources
    await closeTransports(transports.streamable);
    Logger.log("Server shutdown complete");
    process.exit(0);
  });
}
async function closeTransports(
  transports: Record<string, StreamableHTTPServerTransport>,
) {
  for (const sessionId in transports) {
    try {
      await transports[sessionId]?.close();
      delete transports[sessionId];
    } catch (error) {
      console.error(`Error closing transport for session ${sessionId}:`, error);
    }
  }
}
export async function stopHttpServer(): Promise<void> {
  if (!httpServer) {
    throw new Error("HTTP server is not running");
  }
  return new Promise((resolve, reject) => {
    httpServer!.close((err: Error | undefined) => {
      if (err) {
        reject(err);
        return;
      }
      httpServer = null;
      const closing = Object.values(transports.streamable).map((transport) => {
        return transport.close();
      });
      Promise.all(closing).then(() => {
        resolve();
      });
    });
  });
}
```
--------------------------------------------------------------------------------
/src/extractors/flutter/global-vars.ts:
--------------------------------------------------------------------------------
```typescript
// src/extractors/flutter/global-vars.ts
import { FlutterStyleLibrary, FlutterStyleDefinition, StyleRelationship, OptimizationReport } from './style-library.js';
import { Logger } from '../../utils/logger.js';
export interface GlobalVars {
  styles: Record<string, FlutterStyleDefinition>;
  relationships: Record<string, StyleRelationship>;
  usage: Record<string, number>;
}
export class GlobalStyleManager {
  private globalVars: GlobalVars = { styles: {}, relationships: {}, usage: {} };
  private styleLibrary = FlutterStyleLibrary.getInstance();
  
  addStyle(properties: any, context?: string): string {
    Logger.info(`🌐 GlobalStyleManager: Adding style with context: ${context}`);
    
    // Check for exact matches first
    const exactMatch = this.findExactMatch(properties);
    if (exactMatch) {
      Logger.info(`🎯 GlobalStyleManager: Found exact match ${exactMatch}`);
      this.incrementUsage(exactMatch);
      return exactMatch;
    }
    
    // Check for semantic equivalents
    const semanticMatch = this.findSemanticMatch(properties);
    if (semanticMatch) {
      Logger.info(`🔍 GlobalStyleManager: Found semantic match ${semanticMatch}`);
      this.incrementUsage(semanticMatch);
      return semanticMatch;
    }
    
    // Check if this should be a variant of existing style
    const parentStyle = this.findPotentialParent(properties);
    if (parentStyle) {
      Logger.info(`👨👩👧👦 GlobalStyleManager: Found potential parent ${parentStyle.id}`);
    }
    
    // Create new style with proper relationships
    return this.createNewStyle(properties, parentStyle, context);
  }
  
  private findExactMatch(properties: any): string | undefined {
    const hash = this.generateHash(properties);
    
    for (const [id, style] of Object.entries(this.globalVars.styles)) {
      if (style.hash === hash) {
        return id;
      }
    }
    
    return undefined;
  }
  
  private findSemanticMatch(properties: any): string | undefined {
    const semanticHash = this.generateSemanticHash(properties);
    
    for (const [id, style] of Object.entries(this.globalVars.styles)) {
      if (style.semanticHash === semanticHash) {
        return id;
      }
    }
    
    return undefined;
  }
  
  private findPotentialParent(properties: any, threshold: number = 0.8): FlutterStyleDefinition | undefined {
    let bestMatch: FlutterStyleDefinition | undefined;
    let bestSimilarity = 0;
    
    for (const style of Object.values(this.globalVars.styles)) {
      const similarity = this.calculateSimilarity(properties, style.properties);
      if (similarity >= threshold && similarity < 1.0 && similarity > bestSimilarity) {
        bestMatch = style;
        bestSimilarity = similarity;
      }
    }
    
    return bestMatch;
  }
  
  private createNewStyle(properties: any, parentStyle?: FlutterStyleDefinition, context?: string): string {
    // Determine category from context or properties
    const category = this.determineCategory(properties, context);
    
    // Use the style library to create the style (it handles all the logic)
    const styleId = this.styleLibrary.addStyle(category, properties, context);
    const newStyle = this.styleLibrary.getStyle(styleId)!;
    
    // Update global vars
    this.globalVars.styles[styleId] = newStyle;
    this.globalVars.usage[styleId] = newStyle.usageCount;
    
    if (newStyle.parentId || newStyle.childIds.length > 0) {
      this.globalVars.relationships[styleId] = {
        parentId: newStyle.parentId,
        childIds: newStyle.childIds,
        variance: newStyle.variance || 0
      };
    }
    
    return styleId;
  }
  
  private determineCategory(properties: any, context?: string): string {
    if (context) return context;
    
    // Auto-detect category based on properties
    if (properties.fills || properties.cornerRadius || properties.effects) {
      return 'decoration';
    }
    
    if (properties.fontFamily || properties.fontSize || properties.fontWeight) {
      return 'text';
    }
    
    if (properties.padding) {
      return 'padding';
    }
    
    return 'layout';
  }
  
  private incrementUsage(styleId: string): void {
    if (this.globalVars.usage[styleId]) {
      this.globalVars.usage[styleId]++;
    } else {
      this.globalVars.usage[styleId] = 1;
    }
    
    // Also update the style library
    const style = this.styleLibrary.getStyle(styleId);
    if (style) {
      style.usageCount++;
    }
  }
  
  optimizeLibrary(): OptimizationReport {
    Logger.info(`🔧 GlobalStyleManager: Starting library optimization`);
    
    // Sync with style library first
    this.syncWithStyleLibrary();
    Logger.info(`🔄 GlobalStyleManager: Synced with style library`);
    
    // Run optimization on the style library
    const report = this.styleLibrary.optimizeLibrary();
    Logger.info(`📊 GlobalStyleManager: Optimization report:`, report);
    
    // Update global vars after optimization
    this.syncFromStyleLibrary();
    Logger.info(`✅ GlobalStyleManager: Optimization complete`);
    
    return report;
  }
  
  private syncWithStyleLibrary(): void {
    // Update style library with any changes from global vars
    const allStyles = this.styleLibrary.getAllStyles();
    
    for (const style of allStyles) {
      if (this.globalVars.usage[style.id] && this.globalVars.usage[style.id] !== style.usageCount) {
        style.usageCount = this.globalVars.usage[style.id];
      }
    }
  }
  
  private syncFromStyleLibrary(): void {
    // Update global vars with current state of style library
    const allStyles = this.styleLibrary.getAllStyles();
    
    this.globalVars.styles = {};
    this.globalVars.usage = {};
    this.globalVars.relationships = {};
    
    for (const style of allStyles) {
      this.globalVars.styles[style.id] = style;
      this.globalVars.usage[style.id] = style.usageCount;
      
      if (style.parentId || style.childIds.length > 0) {
        this.globalVars.relationships[style.id] = {
          parentId: style.parentId,
          childIds: style.childIds,
          variance: style.variance || 0
        };
      }
    }
  }
  
  getGlobalVars(): GlobalVars {
    this.syncFromStyleLibrary();
    return { ...this.globalVars };
  }
  
  getStyleHierarchy(): Record<string, StyleRelationship> {
    return { ...this.globalVars.relationships };
  }
  
  getUsageStats(): Record<string, number> {
    return { ...this.globalVars.usage };
  }
  
  reset(): void {
    this.globalVars = { styles: {}, relationships: {}, usage: {} };
    this.styleLibrary.reset();
  }
  
  // Helper methods (delegated to style library for consistency)
  private generateHash(properties: any): string {
    return JSON.stringify(properties, Object.keys(properties).sort());
  }
  
  private generateSemanticHash(properties: any): string {
    // This should match the logic in FlutterStyleLibrary
    // For now, delegate to a simplified version
    const normalized = this.normalizeProperties(properties);
    const semanticKey = this.createSemanticKey(normalized);
    return JSON.stringify(semanticKey, Object.keys(semanticKey).sort());
  }
  
  private normalizeProperties(properties: any): any {
    const normalized = { ...properties };
    
    // Normalize color representations
    if (normalized.fills) {
      normalized.fills = normalized.fills.map((fill: any) => {
        if (fill.hex) {
          const hex = fill.hex.toLowerCase();
          if (hex === '#000000') return { ...fill, hex: '#000', normalized: 'black' };
          if (hex === '#ffffff') return { ...fill, hex: '#fff', normalized: 'white' };
        }
        return fill;
      });
    }
    
    // Normalize padding representations
    if (normalized.padding) {
      const p = normalized.padding;
      if (p.top === p.right && p.right === p.bottom && p.bottom === p.left) {
        normalized.padding = { uniform: p.top, isUniform: true };
      }
    }
    
    return normalized;
  }
  
  private createSemanticKey(properties: any): any {
    const key: any = {};
    
    if (properties.fills) {
      key.color = properties.fills[0]?.normalized || properties.fills[0]?.hex;
    }
    
    if (properties.cornerRadius !== undefined) {
      key.borderRadius = typeof properties.cornerRadius === 'number' 
        ? properties.cornerRadius 
        : 'complex';
    }
    
    if (properties.padding) {
      key.padding = properties.padding.isUniform 
        ? properties.padding.uniform 
        : 'complex';
    }
    
    return key;
  }
  
  private calculateSimilarity(props1: any, props2: any): number {
    const keys1 = new Set(Object.keys(props1));
    const keys2 = new Set(Object.keys(props2));
    const allKeys = new Set([...keys1, ...keys2]);
    
    let matches = 0;
    let total = allKeys.size;
    
    for (const key of allKeys) {
      if (keys1.has(key) && keys2.has(key)) {
        if (this.areValuesSimilar(props1[key], props2[key])) {
          matches++;
        }
      }
    }
    
    return total > 0 ? matches / total : 0;
  }
  
  private areValuesSimilar(val1: any, val2: any): boolean {
    if (val1 === val2) return true;
    
    if (Array.isArray(val1) && Array.isArray(val2)) {
      if (val1.length !== val2.length) return false;
      return val1.every((item, index) => this.areValuesSimilar(item, val2[index]));
    }
    
    if (typeof val1 === 'object' && typeof val2 === 'object' && val1 !== null && val2 !== null) {
      const keys1 = Object.keys(val1);
      const keys2 = Object.keys(val2);
      if (keys1.length !== keys2.length) return false;
      return keys1.every(key => this.areValuesSimilar(val1[key], val2[key]));
    }
    
    if (typeof val1 === 'number' && typeof val2 === 'number') {
      return Math.abs(val1 - val2) < 0.01;
    }
    
    return false;
  }
}
```
--------------------------------------------------------------------------------
/src/extractors/flutter/style-merger.ts:
--------------------------------------------------------------------------------
```typescript
// src/extractors/flutter/style-merger.ts
import { FlutterStyleDefinition } from './style-library.js';
import { Logger } from '../../utils/logger.js';
export interface MergeCandidate {
  styles: FlutterStyleDefinition[];
  commonProperties: any;
  differences: Record<string, any[]>;
  mergeScore: number;
}
export class StyleMerger {
  
  canMerge(style1: FlutterStyleDefinition, style2: FlutterStyleDefinition): boolean {
    // Check if two styles can be merged
    if (style1.category !== style2.category) {
      return false;
    }
    
    // Calculate merge benefit
    const compatibility = this.calculateCompatibility(style1.properties, style2.properties);
    const benefit = this.calculateMergeBenefit(style1, style2);
    
    // Merge if compatibility is high and benefit is significant
    return compatibility > 0.7 && benefit > 0.5;
  }
  
  mergeStyles(styles: FlutterStyleDefinition[]): FlutterStyleDefinition | null {
    if (styles.length < 2) return null;
    
    // Find common properties across all styles
    const commonProperties = this.extractCommonProperties(styles);
    
    if (Object.keys(commonProperties).length === 0) {
      return null; // No common properties to merge
    }
    
    // Create merged style from common properties
    const mergedStyle: FlutterStyleDefinition = {
      id: this.generateMergedId(styles),
      category: styles[0].category,
      properties: commonProperties,
      flutterCode: this.generateFlutterCode(styles[0].category, commonProperties),
      hash: this.generateHash(commonProperties),
      semanticHash: this.generateSemanticHash(commonProperties),
      usageCount: styles.reduce((sum, style) => sum + style.usageCount, 0),
      childIds: styles.map(style => style.id),
      variance: 0 // Base style has no variance
    };
    
    return mergedStyle;
  }
  
  extractCommonProperties(styles: FlutterStyleDefinition[]): any {
    if (styles.length === 0) return {};
    
    const commonProps: any = {};
    const firstStyle = styles[0];
    
    // Check each property in the first style
    for (const [key, value] of Object.entries(firstStyle.properties)) {
      let isCommon = true;
      
      // Check if this property exists and has the same value in all other styles
      for (let i = 1; i < styles.length; i++) {
        const otherStyle = styles[i];
        if (!otherStyle.properties.hasOwnProperty(key) || 
            !this.areValuesSimilar(value, otherStyle.properties[key])) {
          isCommon = false;
          break;
        }
      }
      
      if (isCommon) {
        commonProps[key] = value;
      }
    }
    
    return commonProps;
  }
  
  findMergeCandidates(styles: FlutterStyleDefinition[], minScore: number = 0.6): MergeCandidate[] {
    Logger.info(`🔍 StyleMerger: Finding merge candidates from ${styles.length} styles (min score: ${minScore})`);
    const candidates: MergeCandidate[] = [];
    
    // Group styles by category first
    const stylesByCategory = this.groupByCategory(styles);
    Logger.info(`📂 StyleMerger: Grouped styles by category:`, Object.keys(stylesByCategory).map(cat => `${cat}: ${stylesByCategory[cat].length}`));
    
    for (const [category, categoryStyles] of Object.entries(stylesByCategory)) {
      if (categoryStyles.length < 2) continue;
      
      // Find all possible combinations of 2 or more styles
      for (let i = 0; i < categoryStyles.length; i++) {
        for (let j = i + 1; j < categoryStyles.length; j++) {
          const styleGroup = [categoryStyles[i], categoryStyles[j]];
          const candidate = this.analyzeMergeCandidate(styleGroup);
          
          if (candidate.mergeScore >= minScore) {
            candidates.push(candidate);
          }
        }
      }
      
      // Also check for larger groups (3+ styles)
      if (categoryStyles.length >= 3) {
        const largeGroupCandidate = this.analyzeMergeCandidate(categoryStyles);
        if (largeGroupCandidate.mergeScore >= minScore) {
          candidates.push(largeGroupCandidate);
        }
      }
    }
    
    // Sort by merge score (highest first)
    return candidates.sort((a, b) => b.mergeScore - a.mergeScore);
  }
  
  private analyzeMergeCandidate(styles: FlutterStyleDefinition[]): MergeCandidate {
    const commonProperties = this.extractCommonProperties(styles);
    const differences = this.extractDifferences(styles, commonProperties);
    const mergeScore = this.calculateMergeScore(styles, commonProperties, differences);
    
    return {
      styles,
      commonProperties,
      differences,
      mergeScore
    };
  }
  
  private extractDifferences(styles: FlutterStyleDefinition[], commonProperties: any): Record<string, any[]> {
    const differences: Record<string, any[]> = {};
    
    // Collect all unique property keys
    const allKeys = new Set<string>();
    styles.forEach(style => {
      Object.keys(style.properties).forEach(key => allKeys.add(key));
    });
    
    // Find differences for each key
    for (const key of allKeys) {
      if (commonProperties.hasOwnProperty(key)) continue; // Skip common properties
      
      const values = styles.map(style => style.properties[key]).filter(val => val !== undefined);
      if (values.length > 0) {
        differences[key] = values;
      }
    }
    
    return differences;
  }
  
  private calculateMergeScore(styles: FlutterStyleDefinition[], commonProperties: any, differences: Record<string, any[]>): number {
    const totalProperties = this.countTotalProperties(styles);
    const commonCount = Object.keys(commonProperties).length;
    const differenceCount = Object.keys(differences).length;
    
    if (totalProperties === 0) return 0;
    
    // Score based on ratio of common properties to total properties
    const commonRatio = commonCount / totalProperties;
    
    // Bonus for high usage styles (more benefit from merging)
    const usageBonus = styles.reduce((sum, style) => sum + style.usageCount, 0) / (styles.length * 10);
    
    // Penalty for too many differences (harder to maintain)
    const differencePenalty = Math.min(differenceCount / 10, 0.3);
    
    return Math.max(0, Math.min(1, commonRatio + usageBonus - differencePenalty));
  }
  
  private countTotalProperties(styles: FlutterStyleDefinition[]): number {
    const allKeys = new Set<string>();
    styles.forEach(style => {
      Object.keys(style.properties).forEach(key => allKeys.add(key));
    });
    return allKeys.size;
  }
  
  private groupByCategory(styles: FlutterStyleDefinition[]): Record<string, FlutterStyleDefinition[]> {
    const groups: Record<string, FlutterStyleDefinition[]> = {};
    
    styles.forEach(style => {
      if (!groups[style.category]) {
        groups[style.category] = [];
      }
      groups[style.category].push(style);
    });
    
    return groups;
  }
  
  private calculateCompatibility(props1: any, props2: any): number {
    const keys1 = new Set(Object.keys(props1));
    const keys2 = new Set(Object.keys(props2));
    const allKeys = new Set([...keys1, ...keys2]);
    
    let compatible = 0;
    let total = allKeys.size;
    
    for (const key of allKeys) {
      if (keys1.has(key) && keys2.has(key)) {
        // Both have the key - check if values are compatible
        if (this.areValuesSimilar(props1[key], props2[key])) {
          compatible++;
        }
      } else if (keys1.has(key) || keys2.has(key)) {
        // Only one has the key - partial compatibility
        compatible += 0.5;
      }
    }
    
    return total > 0 ? compatible / total : 0;
  }
  
  private calculateMergeBenefit(style1: FlutterStyleDefinition, style2: FlutterStyleDefinition): number {
    // Benefit is higher for frequently used styles
    const usageBenefit = (style1.usageCount + style2.usageCount) / 20; // Normalize to 0-1 range
    
    // Benefit is higher for similar styles (less information loss)
    const similarity = this.calculateSimilarity(style1.properties, style2.properties);
    
    return Math.min(1, (usageBenefit + similarity) / 2);
  }
  
  private calculateSimilarity(props1: any, props2: any): number {
    const keys1 = new Set(Object.keys(props1));
    const keys2 = new Set(Object.keys(props2));
    const allKeys = new Set([...keys1, ...keys2]);
    
    let matches = 0;
    let total = allKeys.size;
    
    for (const key of allKeys) {
      if (keys1.has(key) && keys2.has(key)) {
        if (this.areValuesSimilar(props1[key], props2[key])) {
          matches++;
        }
      }
    }
    
    return total > 0 ? matches / total : 0;
  }
  
  private areValuesSimilar(val1: any, val2: any): boolean {
    if (val1 === val2) return true;
    
    if (Array.isArray(val1) && Array.isArray(val2)) {
      if (val1.length !== val2.length) return false;
      return val1.every((item, index) => this.areValuesSimilar(item, val2[index]));
    }
    
    if (typeof val1 === 'object' && typeof val2 === 'object' && val1 !== null && val2 !== null) {
      const keys1 = Object.keys(val1);
      const keys2 = Object.keys(val2);
      if (keys1.length !== keys2.length) return false;
      return keys1.every(key => this.areValuesSimilar(val1[key], val2[key]));
    }
    
    if (typeof val1 === 'number' && typeof val2 === 'number') {
      return Math.abs(val1 - val2) < 0.01;
    }
    
    return false;
  }
  
  private generateMergedId(styles: FlutterStyleDefinition[]): string {
    const category = styles[0].category;
    const timestamp = Date.now().toString(36);
    const hash = Math.random().toString(36).substr(2, 4);
    return `${category}Merged${timestamp}${hash}`;
  }
  
  private generateFlutterCode(category: string, properties: any): string {
    // This should use the same logic as FlutterCodeGenerator
    // For now, return a placeholder
    return `// Merged ${category} style`;
  }
  
  private generateHash(properties: any): string {
    return JSON.stringify(properties, Object.keys(properties).sort());
  }
  
  private generateSemanticHash(properties: any): string {
    // Simplified semantic hash for merged styles
    const keys = Object.keys(properties).sort();
    return `semantic_${keys.join('_')}_${this.generateHash(properties)}`;
  }
}
```
--------------------------------------------------------------------------------
/src/extractors/typography/extractor.ts:
--------------------------------------------------------------------------------
```typescript
// src/extractors/typography/extractor.mts
import type {FigmaNode, FigmaTextStyle} from '../../types/figma.js';
import type {
    TypographyStyle,
    TypographyDefinition,
    TypographyExtractionContext,
    TypographyExtractorFn,
    TextStyleHash
} from './types.js';
/**
 * Extract typography styles from a theme frame containing text samples
 */
export function extractTypographyFromThemeFrame(frameNode: FigmaNode): TypographyStyle[] {
    const typography: TypographyStyle[] = [];
    // Find all child nodes in the frame
    if (!frameNode.children) {
        return typography;
    }
    // Look for text nodes only, ignore frames and other nodes that might be for colors
    frameNode.children.forEach(child => {
        // Only process direct text nodes or groups/frames that contain text
        if (child.type === 'TEXT') {
            const textStyle = createTypographyStyle(child);
            if (textStyle) {
                typography.push(textStyle);
            }
        } else if (child.children) {
            // For groups/frames, only look for immediate text children
            child.children.forEach(grandchild => {
                if (grandchild.type === 'TEXT') {
                    const textStyle = createTypographyStyle(grandchild);
                    if (textStyle) {
                        typography.push(textStyle);
                    }
                }
            });
        }
    });
    return typography;
}
/**
 * Extract typography from a single node (used by theme frame extraction)
 */
function extractTypographyFromNode(node: FigmaNode): TypographyStyle | null {
    // Check if this node is a text node
    if (node.type === 'TEXT' && node.style) {
        return createTypographyStyle(node);
    }
    // Only check immediate children for text nodes, don't go deep to avoid color frames
    if (node.children) {
        for (const child of node.children) {
            // Only process direct text nodes, ignore nested structures
            if (child.type === 'TEXT' && child.style) {
                return createTypographyStyle(child);
            }
        }
    }
    return null;
}
/**
 * Create typography style from text node
 */
function createTypographyStyle(node: FigmaNode): TypographyStyle | null {
    if (!node.style) {
        return null;
    }
    // Get typography name from node name or generate one
    const typographyName = getTypographyName(node);
    if (!typographyName) {
        return null;
    }
    // Ensure we have a valid font family, fallback to system default if needed
    const fontFamily = node.style.fontFamily && node.style.fontFamily.trim() !== ''
        ? node.style.fontFamily.trim()
        : 'Roboto'; // Flutter's default font
    return {
        name: typographyName,
        fontFamily: fontFamily,
        fontSize: node.style.fontSize || 16,
        fontWeight: node.style.fontWeight || 400,
        lineHeight: node.style.lineHeightPx || (node.style.fontSize || 16) * 1.2,
        letterSpacing: node.style.letterSpacing || 0,
        nodeId: node.id,
        textAlign: node.style.textAlignHorizontal,
    };
}
/**
 * Get typography name from node or generate meaningful name
 */
function getTypographyName(node: FigmaNode): string | null {
    // Always prioritize the actual node name from Figma first
    if (node.name && node.name.trim() && !isTypographyDemoNode(node)) {
        return cleanTypographyName(node.name);
    }
    // Fallback to style-based generation only if no meaningful name
    if (node.style) {
        return generateTypographyNameFromStyle(node.style);
    }
    return null;
}
/**
 * Check if name looks like a typography style name
 */
function isTypographyStyleName(name: string): boolean {
    const typographyKeywords = [
        // Typography categories
        'heading', 'title', 'subtitle', 'body', 'caption', 'label', 'button',
        'display', 'headline', 'subheading', 'overline',
        // Size variations
        'large', 'medium', 'small', 'xl', 'lg', 'md', 'sm', 'xs',
        'big', 'regular', 'tiny',
        // Weight variations
        'bold', 'semibold', 'medium', 'regular', 'light', 'thin',
        // Number variations
        'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
        '1', '2', '3', '4', '5', '6'
    ];
    const lowerName = name.toLowerCase();
    return typographyKeywords.some(keyword => lowerName.includes(keyword));
}
/**
 * Check if node is a typography demo/sample and should be excluded
 */
export function isTypographyDemoNode(node: FigmaNode): boolean {
    const demoKeywords = [
        // Lorem ipsum and common demo text
        'lorem', 'ipsum', 'dolor', 'sit', 'amet',
        'sample text', 'example text', 'demo text', 'placeholder text',
        'the quick brown', 'abcdefg', 'test text',
        // Demo indicators
        'sample', 'example', 'demo', 'placeholder', 'preview',
        'specimen', 'showcase', 'test'
    ];
    const nodeName = node.name.toLowerCase();
    // Check node name against demo keywords
    const isDemoByName = demoKeywords.some(keyword =>
        nodeName.includes(keyword.toLowerCase())
    );
    // Check if node name is very generic (likely demo text)
    const isGenericName = /^(text|label|title|heading|body)(\s*\d+)?$/i.test(node.name.trim());
    return isDemoByName || isGenericName;
}
/**
 * Clean up typography name for use as identifier while preserving original naming
 */
function cleanTypographyName(name: string): string {
    // First, clean up the name but preserve the original structure
    let cleaned = name
        .trim()
        .replace(/\s+/g, ' '); // Normalize whitespace
    // Only remove common prefixes/suffixes if they're clearly not part of the intended name
    if (cleaned.toLowerCase().startsWith('text ') && cleaned.split(' ').length > 1) {
        cleaned = cleaned.substring(5);
    }
    if (cleaned.toLowerCase().endsWith(' style') && cleaned.split(' ').length > 1) {
        cleaned = cleaned.substring(0, cleaned.length - 6);
    }
    // Convert to PascalCase while preserving meaningful word boundaries
    return cleaned
        .split(/[\s\-_]+/)
        .filter(word => word.length > 0)
        .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
        .join('');
}
/**
 * Generate typography name from style properties
 */
function generateTypographyNameFromStyle(style: FigmaTextStyle): string {
    const fontSize = style.fontSize || 16;
    const fontWeight = style.fontWeight || 400;
    // Categorize by size
    let sizeName = 'Body';
    if (fontSize >= 32) sizeName = 'DisplayLarge';
    else if (fontSize >= 28) sizeName = 'DisplayMedium';
    else if (fontSize >= 24) sizeName = 'DisplaySmall';
    else if (fontSize >= 22) sizeName = 'HeadlineLarge';
    else if (fontSize >= 20) sizeName = 'HeadlineMedium';
    else if (fontSize >= 18) sizeName = 'HeadlineSmall';
    else if (fontSize >= 16) sizeName = 'BodyLarge';
    else if (fontSize >= 14) sizeName = 'BodyMedium';
    else if (fontSize >= 12) sizeName = 'BodySmall';
    else if (fontSize >= 11) sizeName = 'LabelLarge';
    else if (fontSize >= 10) sizeName = 'LabelMedium';
    else sizeName = 'LabelSmall';
    // Add weight if not regular
    if (fontWeight >= 700) {
        sizeName += 'Bold';
    } else if (fontWeight >= 600) {
        sizeName += 'SemiBold';
    } else if (fontWeight >= 500) {
        sizeName += 'Medium';
    } else if (fontWeight <= 300) {
        sizeName += 'Light';
    }
    return sizeName;
}
/**
 * Generate meaningful typography name from style and context
 */
function generateTypographyName(style: FigmaTextStyle, nodeName: string): string {
    // Try to infer name from node context first
    const name = nodeName.toLowerCase();
    if (name.includes('heading') || name.includes('title')) return 'Heading';
    if (name.includes('subtitle')) return 'Subtitle';
    if (name.includes('body')) return 'Body';
    if (name.includes('caption')) return 'Caption';
    if (name.includes('button')) return 'Button';
    if (name.includes('label')) return 'Label';
    // Fallback to style-based naming
    return generateTypographyNameFromStyle(style);
}
/**
 * Generate Dart-safe property name
 */
function generateDartSafeName(style: FigmaTextStyle, nodeName: string): string {
    const baseName = generateTypographyName(style, nodeName);
    // Convert to camelCase and ensure it's Dart-safe
    return baseName.charAt(0).toLowerCase() + baseName.slice(1)
        .replace(/[^a-zA-Z0-9]/g, '')
        .replace(/^\d/, '_$&'); // Prefix with underscore if starts with number
}
/**
 * Categorize typography usage
 */
function categorizeTypographyUsage(style: FigmaTextStyle, nodeName: string): TypographyDefinition['usage'] {
    const name = nodeName.toLowerCase();
    const fontSize = style.fontSize || 16;
    // Check name patterns first
    if (name.includes('heading') || name.includes('title') || name.includes('h1') || name.includes('h2') || name.includes('h3')) {
        return 'heading';
    }
    if (name.includes('body') || name.includes('paragraph')) return 'body';
    if (name.includes('caption') || name.includes('small')) return 'caption';
    if (name.includes('button')) return 'button';
    if (name.includes('label')) return 'label';
    // Fallback to size-based categorization
    if (fontSize >= 20) return 'heading';
    if (fontSize >= 14) return 'body';
    if (fontSize >= 12) return 'caption';
    return 'other';
}
/**
 * Create text style hash for deduplication
 */
function createTextStyleHash(style: FigmaTextStyle): TextStyleHash {
    return {
        fontFamily: style.fontFamily || 'default',
        fontSize: style.fontSize || 16,
        fontWeight: style.fontWeight || 400,
        lineHeight: style.lineHeightPx || (style.fontSize || 16) * 1.2,
        letterSpacing: style.letterSpacing || 0,
    };
}
/**
 * Add typography to library with deduplication
 */
function addTypographyToLibrary(
    style: FigmaTextStyle,
    nodeName: string,
    typographyLibrary: TypographyDefinition[],
    typographyMap: Map<string, string>
): string {
    const styleHash = createTextStyleHash(style);
    const hashString = JSON.stringify(styleHash);
    // Check if typography already exists
    const existingId = typographyMap.get(hashString);
    if (existingId) {
        // Increment usage count
        const typography = typographyLibrary.find(t => t.id === existingId);
        if (typography) {
            typography.usageCount++;
        }
        return existingId;
    }
    // Create new typography definition
    const typographyId = `typography_${typographyLibrary.length + 1}`;
    const typographyDef: TypographyDefinition = {
        id: typographyId,
        name: generateTypographyName(style, nodeName),
        fontFamily: style.fontFamily && style.fontFamily.trim() !== ''
            ? style.fontFamily.trim()
            : 'Roboto',
        fontSize: style.fontSize || 16,
        fontWeight: style.fontWeight || 400,
        lineHeight: style.lineHeightPx || (style.fontSize || 16) * 1.2,
        letterSpacing: style.letterSpacing || 0,
        usage: categorizeTypographyUsage(style, nodeName),
        usageCount: 1,
        dartName: generateDartSafeName(style, nodeName)
    };
    typographyLibrary.push(typographyDef);
    typographyMap.set(hashString, typographyId);
    return typographyId;
}
```