diff --git a/README.md b/README.md index d7b32d2..560c87c 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,25 @@ # Adobe XD MCP Server -A Model Context Protocol (MCP) server that enables Claude to interact with Adobe XD files, extract design information, and generate React components from your designs. +A Model Context Protocol (MCP) server that enables Claude to interact with Adobe XD files, extract design information, and generate pixel-perfect React components from your designs. ## Features +### Core Features - **Document Analysis**: Extract comprehensive information from XD files including artboards, components, and assets - **React Component Generation**: Convert XD designs to React components with multiple styling options - **Color Extraction**: Extract color palettes in various formats (CSS, JSON, Tailwind) - **Multiple Style Systems**: Support for styled-components, Tailwind CSS, and CSS modules - **TypeScript Support**: Generate type-safe React components +### Pixel-Perfect Design Analysis ๐ŸŽฏ +- **Layout & Spacing Analysis**: Automatically detect spacing patterns and generate spacing tokens +- **Design Token Generation**: Extract complete design systems (colors, spacing, typography) in multiple formats +- **UI Pattern Detection**: Intelligently identify buttons, cards, and other UI components +- **Typography System Extraction**: Analyze and organize all text styles into a reusable type system +- **Layout System Detection**: Detect flexbox/grid patterns and generate implementation CSS +- **Element Style Extraction**: Get detailed CSS and Tailwind classes for any element +- **Artboard Preview Export**: View design previews directly in the terminal + ## Prerequisites - Node.js (v16 or higher) @@ -36,7 +46,7 @@ npm run build ## Configuration -Add the server to your Claude Desktop configuration: +Add the server to your Claude Code configuration: ### macOS Edit: `~/Library/Application Support/Claude/claude_desktop_config.json` @@ -44,6 +54,9 @@ Edit: `~/Library/Application Support/Claude/claude_desktop_config.json` ### Windows Edit: `%APPDATA%\Claude\claude_desktop_config.json` +### Linux/WSL +Edit: `~/.claude.json` + Add the following configuration: ```json @@ -59,74 +72,298 @@ Add the following configuration: **Note**: Replace `/absolute/path/to/adobe-xd-mcp` with the actual path to your installation directory. -## Usage +## Available Tools + +### 1. get_xd_info +Get comprehensive information about an XD document. -After configuring and restarting Claude Desktop, you can use the following commands: +**Returns:** +- Document name and metadata +- List of artboards with dimensions and element counts +- Components and their structures +- Color palette -### Get XD Document Information +### 2. generate_react_component +Convert XD artboards/components to React code. -Ask Claude to analyze your XD file: +**Parameters:** +- `path`: Path to XD file (required) +- `artboardName`: Specific artboard to convert (optional) +- `componentName`: Specific component to convert (optional) +- `outputDir`: Output directory (optional) +- `styleSystem`: "styled-components", "tailwind", or "css-modules" (default: tailwind) +- `typescript`: Generate TypeScript (default: true) + +### 3. extract_colors +Extract color palette in various formats. + +**Parameters:** +- `path`: Path to XD file (required) +- `format`: "css", "json", or "tailwind" (default: css) +- `outputFile`: Save to file (optional) + +### 4. get_artboard_details +Get detailed information about a specific artboard including all element positions, sizes, and styles. + +**Parameters:** +- `path`: Path to XD file (required) +- `artboardName`: Name of artboard (required) + +### 5. get_element_styles +Get detailed CSS properties and Tailwind classes for elements. + +**Parameters:** +- `path`: Path to XD file (required) +- `artboardName`: Name of artboard (required) +- `elementName`: Specific element name (optional) + +**Returns:** +- CSS properties for each element +- Suggested Tailwind classes +- Positioning and sizing information + +### 6. analyze_layout_spacing โญ +Analyze spacing between elements for pixel-perfect implementation. + +**Parameters:** +- `path`: Path to XD file (required) +- `artboardName`: Name of artboard (required) + +**Returns:** +- Spacing tokens with usage frequency +- Suggested semantic names (xs, sm, md, lg, xl) +- Layout direction (horizontal/vertical/mixed) +- Gap values +- Suggested CSS (flexbox/grid) + +### 7. generate_design_tokens โญ +Generate complete design system tokens. + +**Parameters:** +- `path`: Path to XD file (required) +- `format`: "json", "css", "tailwind", or "typescript" (default: json) +- `outputFile`: Save to file (optional) + +**Returns:** +- Spacing tokens +- Color tokens with usage counts +- Typography system +- Border radius values +- Ready-to-use code in your chosen format + +### 8. detect_ui_patterns โญ +Automatically identify common UI components. + +**Parameters:** +- `path`: Path to XD file (required) +- `artboardName`: Name of artboard (required) + +**Returns:** +- Detected patterns (button, card, input, etc.) +- Confidence scores +- Bounding boxes +- Suggested component names + +### 9. extract_typography_system โญ +Extract and organize all typography styles. + +**Parameters:** +- `path`: Path to XD file (required) + +**Returns:** +- All unique font combinations +- Usage frequency for each style +- Suggested semantic names (display, h1-h5, body, etc.) +- Font properties (family, size, weight, line-height, letter-spacing) + +### 10. detect_layout_system โญ +Detect layout patterns and generate implementation CSS. + +**Parameters:** +- `path`: Path to XD file (required) +- `artboardName`: Name of artboard (required) + +**Returns:** +- Layout direction +- Detected gaps +- Alignment patterns +- Suggested CSS (display, flex-direction, gap, justify-content, etc.) + +### 11. export_artboard_preview +Export artboard as base64-encoded image for viewing. + +**Parameters:** +- `path`: Path to XD file (required) +- `artboardName`: Name of artboard (required) + +## Usage Examples + +### Basic Document Analysis ``` -"Get information about the XD file at /path/to/your/design.xd" +"Get information about the XD file at /path/to/design.xd" ``` -This returns: -- Document name and dimensions -- List of artboards with sizes -- Components and their element counts - -### Generate React Components +### Generate React Component +``` +"Generate a React component from the LoginScreen artboard in /path/to/design.xd using TypeScript and Tailwind CSS" +``` -Convert XD designs to React components: +### Extract Design Tokens ``` -"Generate a React component from the LoginScreen artboard in /path/to/design.xd" +"Generate design tokens from /path/to/design.xd in Tailwind format and save to tokens.js" ``` -Options: -- `artboardName`: Specific artboard to convert (optional) -- `componentName`: Custom component name (optional) -- `outputDir`: Output directory (optional, defaults to ./generated) -- `styleSystem`: Choose from "styled-components", "tailwind", or "css-modules" -- `typescript`: Generate TypeScript components (true/false) +### Analyze Spacing for Pixel-Perfect Implementation +``` +"Analyze the spacing in the HomePage artboard from /path/to/design.xd" +``` -### Extract Colors +Returns spacing tokens like: +```json +{ + "spacingTokens": [ + { "value": 8, "usage": 15, "suggestedName": "sm" }, + { "value": 16, "usage": 12, "suggestedName": "base" }, + { "value": 24, "usage": 8, "suggestedName": "lg" } + ], + "layoutAnalysis": { + "direction": "vertical", + "gaps": [16, 24, 16], + "suggestedCSS": { + "display": "flex", + "flexDirection": "column", + "gap": "16px" + } + } +} +``` -Extract color palettes from your designs: +### Detect UI Components ``` -"Extract colors from /path/to/design.xd and save as CSS variables" +"Detect UI patterns in the Dashboard artboard from /path/to/design.xd" ``` -Options: -- `format`: Output format - "css", "json", or "tailwind" -- `outputFile`: Save to file instead of displaying output - -## Examples +Returns identified components: +```json +{ + "patterns": [ + { + "type": "button", + "confidence": 0.8, + "suggestedComponent": "Button", + "bounds": { "x": 100, "y": 200, "width": 120, "height": 40 } + }, + { + "type": "card", + "confidence": 0.6, + "suggestedComponent": "Card", + "bounds": { "x": 0, "y": 0, "width": 300, "height": 400 } + } + ] +} +``` -### Example 1: Generate a TypeScript React component with Tailwind +### Extract Typography System ``` -"Generate a React component from the Header artboard in design.xd using TypeScript and Tailwind CSS" +"Extract the typography system from /path/to/design.xd" ``` -### Example 2: Extract colors as Tailwind config -``` -"Extract colors from design.xd in Tailwind format and save to colors.js" +Returns: +```json +{ + "styles": [ + { + "fontSize": 48, + "fontFamily": "Inter", + "fontWeight": 700, + "lineHeight": 1.2, + "usage": 5, + "suggestedName": "display" + }, + { + "fontSize": 16, + "fontFamily": "Inter", + "fontWeight": 400, + "lineHeight": 1.5, + "usage": 45, + "suggestedName": "body" + } + ] +} ``` -### Example 3: Get overview of all components +### Get Element Styles +``` +"Get the styles for the 'Primary Button' element in the Components artboard" ``` -"Show me all components in my design system at /designs/system.xd" + +Returns CSS and Tailwind classes: +```json +{ + "elements": [ + { + "name": "Primary Button", + "type": "rectangle", + "css": { + "width": "120px", + "height": "40px", + "backgroundColor": "#5ead0c", + "borderRadius": "8px" + }, + "tailwindClasses": ["bg-[#5ead0c]", "rounded-lg"] + } + ] +} ``` +## Pixel-Perfect Implementation Workflow + +1. **Analyze the Design** + ``` + "Analyze layout spacing in my HomePage artboard" + ``` + +2. **Generate Design Tokens** + ``` + "Generate design tokens in TypeScript format" + ``` + +3. **Detect UI Patterns** + ``` + "Detect UI patterns in the HomePage artboard" + ``` + +4. **Extract Typography** + ``` + "Extract typography system from the design" + ``` + +5. **Get Specific Element Styles** + ``` + "Get styles for the header navigation" + ``` + +6. **Generate Component Code** + ``` + "Generate React component for the HomePage using the detected patterns" + ``` + ## Development ### Project Structure ``` adobe-xd-mcp/ โ”œโ”€โ”€ src/ -โ”‚ โ”œโ”€โ”€ index.ts # MCP server setup -โ”‚ โ””โ”€โ”€ tools/ -โ”‚ โ””โ”€โ”€ xd-tools.ts # XD processing logic -โ”œโ”€โ”€ dist/ # Compiled output +โ”‚ โ”œโ”€โ”€ index.ts # MCP server setup +โ”‚ โ”œโ”€โ”€ tools/ +โ”‚ โ”‚ โ””โ”€โ”€ xd-tools.ts # Main XD processing logic +โ”‚ โ”œโ”€โ”€ parsers/ +โ”‚ โ”‚ โ””โ”€โ”€ xd-parser.ts # XD file parsing +โ”‚ โ”œโ”€โ”€ generators/ +โ”‚ โ”‚ โ””โ”€โ”€ react-generator.ts # React code generation +โ”‚ โ””โ”€โ”€ analyzers/ +โ”‚ โ””โ”€โ”€ design-system-analyzer.ts # Design system analysis +โ”œโ”€โ”€ dist/ # Compiled output โ”œโ”€โ”€ package.json โ””โ”€โ”€ tsconfig.json ``` @@ -147,16 +384,33 @@ npm run build 1. Ensure the path in `claude_desktop_config.json` is absolute 2. Restart Claude Desktop after configuration changes 3. Check that the build completed successfully +4. Verify Node.js version is 16 or higher ### XD file parsing errors 1. Ensure the XD file path is correct and accessible 2. Verify the XD file is not corrupted 3. Check file permissions +4. Try re-saving the XD file +5. Note: Supports both legacy (single artwork.agc) and modern (per-artboard graphicContent.agc) XD file formats ### Component generation issues -1. Verify the artboard name exists in the XD file +1. Verify the artboard name exists in the XD file (case-insensitive) 2. Ensure the output directory is writable 3. Check for naming conflicts with existing files +4. Verify sufficient disk space + +### Design token extraction returns empty results +1. Ensure the XD file contains actual design elements +2. Check that artboards have named elements +3. Verify text elements have font properties set + +## Tips for Best Results + +1. **Name Your Elements**: Named elements in XD produce better, more semantic code +2. **Use Consistent Spacing**: Regular spacing values produce cleaner spacing tokens +3. **Organize with Components**: XD components map well to React components +4. **Group Related Elements**: Grouped elements are easier to identify as UI patterns +5. **Use Text Styles**: Consistent text styling produces better typography systems ## Contributing @@ -168,4 +422,22 @@ MIT License - see LICENSE file for details ## Support -For issues and feature requests, please use the GitHub issue tracker. \ No newline at end of file +For issues and feature requests, please use the GitHub issue tracker. + +## Changelog + +### v2.0.0 - Pixel-Perfect Design Analysis +- Added layout and spacing analysis +- Added design token generation (CSS, Tailwind, TypeScript) +- Added UI pattern detection +- Added typography system extraction +- Added layout system detection +- Added element style extraction with Tailwind classes +- Added artboard preview export +- Enhanced XD parser to support both legacy and modern XD file formats + +### v1.0.0 - Initial Release +- XD file parsing (legacy format) +- React component generation +- Color extraction +- Basic design analysis \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3025910..df24a23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,16 +7,16 @@ "": { "name": "adobe-xd-mcp", "version": "1.0.0", - "license": "ISC", + "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.15.1", + "@modelcontextprotocol/sdk": "^0.5.0", "jszip": "^3.10.1" }, "devDependencies": { "@types/jszip": "^3.4.1", - "@types/node": "^24.0.13", - "tsx": "^4.20.3", - "typescript": "^5.8.3" + "@types/node": "^20.0.0", + "tsx": "^4.0.0", + "typescript": "^5.0.0" } }, "node_modules/@esbuild/aix-ppc64": { @@ -436,25 +436,14 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.15.1.tgz", - "integrity": "sha512-W/XlN9c528yYn+9MQkVjxiTPgPxoxt+oczfjHBDsJx0+59+O7B75Zhsp0B16Xbwbz8ANISDajh6+V7nIcPMc5w==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-0.5.0.tgz", + "integrity": "sha512-RXgulUX6ewvxjAG0kOpLMEdXXWkzWgaoCGaA2CwNW7cQCIphjpJhjpHSiaPdVCnisjRF/0Cm9KWHUuIoeiAblQ==", + "license": "MIT", "dependencies": { - "ajv": "^6.12.6", "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" - }, - "engines": { - "node": ">=18" + "zod": "^3.23.8" } }, "node_modules/@types/jszip": { @@ -468,237 +457,47 @@ } }, "node_modules/@types/node": { - "version": "24.0.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.13.tgz", - "integrity": "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==", + "version": "20.19.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.21.tgz", + "integrity": "sha512-CsGG2P3I5y48RPMfprQGfy4JPRZ6csfC3ltBZSRItG3ngggmNY/qs2uZKp4p9VbrpqNNSMzUZNFZKzgOGnd/VA==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~7.8.0" - } - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" - }, - "engines": { - "node": ">=18" + "undici-types": "~6.21.0" } }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/content-type": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "engines": { - "node": ">=6.6.0" - } - }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/esbuild": { "version": "0.25.6", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", @@ -740,135 +539,6 @@ "@esbuild/win32-x64": "0.25.6" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.3.tgz", - "integrity": "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==", - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", - "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -883,49 +553,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/get-tsconfig": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", @@ -938,43 +565,11 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", @@ -986,23 +581,20 @@ "node": ">= 0.8" } }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/immediate": { @@ -1015,34 +607,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" - }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -1062,199 +631,29 @@ "immediate": "~3.0.5" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", - "engines": { - "node": ">=16" - } - }, - "node_modules/pkce-challenge": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", - "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", - "engines": { - "node": ">=16.20.0" - } - }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "license": "MIT", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", - "iconv-lite": "0.6.3", + "iconv-lite": "0.7.0", "unpipe": "1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" } }, "node_modules/readable-stream": { @@ -1285,79 +684,11 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - } + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" }, "node_modules/setimmediate": { "version": "1.0.5", @@ -1367,99 +698,14 @@ "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" }, "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -1481,6 +727,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", "engines": { "node": ">=0.6" } @@ -1504,19 +751,6 @@ "fsevents": "~2.3.3" } }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -1531,74 +765,34 @@ } }, "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", - "dev": true + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } - }, - "node_modules/zod-to-json-schema": { - "version": "3.24.6", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", - "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", - "peerDependencies": { - "zod": "^3.24.1" - } } } } diff --git a/package.json b/package.json index 4c9b98c..7990208 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "adobe-xd-mcp", - "version": "1.0.0", - "description": "MCP server for Adobe XD - read designs and generate React components", + "version": "2.0.0", + "description": "MCP server for Adobe XD - extract design systems and generate pixel-perfect React components", "main": "dist/index.js", "scripts": { "build": "tsc", @@ -17,7 +17,7 @@ "jszip": "^3.10.1" }, "devDependencies": { - "@types/jszip": "^3.10.0", + "@types/jszip": "^3.4.1", "@types/node": "^20.0.0", "tsx": "^4.0.0", "typescript": "^5.0.0" diff --git a/src/analyzers/design-system-analyzer.ts b/src/analyzers/design-system-analyzer.ts new file mode 100644 index 0000000..704505f --- /dev/null +++ b/src/analyzers/design-system-analyzer.ts @@ -0,0 +1,598 @@ +// src/analyzers/design-system-analyzer.ts +import { XDArtboard, XDElement, XDDocument } from '../parsers/xd-parser'; + +export interface SpacingToken { + value: number; + usage: number; + suggestedName?: string; +} + +export interface TypographyStyle { + fontSize: number; + fontFamily?: string; + fontWeight?: number; + lineHeight?: number; + letterSpacing?: number; + color?: string; + usage: number; + suggestedName?: string; +} + +export interface ShadowToken { + x: number; + y: number; + blur: number; + color: string; + usage: number; + suggestedName?: string; + cssShadow: string; +} + +export interface BorderRadiusToken { + value: number; + usage: number; + suggestedName?: string; +} + +export interface OpacityToken { + value: number; + usage: number; + suggestedName?: string; +} + +export interface DesignTokens { + spacing: SpacingToken[]; + colors: { value: string; usage: number; suggestedName?: string }[]; + typography: TypographyStyle[]; + borderRadius: BorderRadiusToken[]; + shadows: ShadowToken[]; + opacity: OpacityToken[]; +} + +export interface LayoutAnalysis { + direction: 'horizontal' | 'vertical' | 'mixed'; + gaps: number[]; + padding: { top: number; right: number; bottom: number; left: number }; + alignment: 'start' | 'center' | 'end' | 'space-between' | 'space-around'; + suggestedCSS: { + display: string; + flexDirection?: string; + gap?: string; + justifyContent?: string; + alignItems?: string; + padding?: string; + }; +} + +export interface UIPattern { + type: 'button' | 'input' | 'card' | 'header' | 'footer' | 'navigation' | 'unknown'; + confidence: number; + elements: XDElement[]; + bounds: { x: number; y: number; width: number; height: number }; + suggestedComponent?: string; +} + +export class DesignSystemAnalyzer { + /** + * Analyze spacing patterns between elements + */ + analyzeSpacing(elements: XDElement[]): number[] { + const gaps: number[] = []; + + // Sort elements by Y position (top to bottom) + const sortedByY = [...elements].sort((a, b) => (a.y || 0) - (b.y || 0)); + + // Calculate vertical gaps + for (let i = 0; i < sortedByY.length - 1; i++) { + const current = sortedByY[i]; + const next = sortedByY[i + 1]; + + const currentBottom = (current.y || 0) + (current.height || 0); + const nextTop = next.y || 0; + const gap = nextTop - currentBottom; + + if (gap > 0 && gap < 200) { // Reasonable spacing range + gaps.push(Math.round(gap)); + } + } + + // Sort elements by X position (left to right) + const sortedByX = [...elements].sort((a, b) => (a.x || 0) - (b.x || 0)); + + // Calculate horizontal gaps + for (let i = 0; i < sortedByX.length - 1; i++) { + const current = sortedByX[i]; + const next = sortedByX[i + 1]; + + const currentRight = (current.x || 0) + (current.width || 0); + const nextLeft = next.x || 0; + const gap = nextLeft - currentRight; + + if (gap > 0 && gap < 200) { + gaps.push(Math.round(gap)); + } + } + + return gaps; + } + + /** + * Extract spacing tokens from gaps + */ + extractSpacingTokens(gaps: number[]): SpacingToken[] { + const tolerance = 2; // pixels + const grouped = new Map(); + + // Group similar values + gaps.forEach(gap => { + let found = false; + for (const [key, count] of grouped.entries()) { + if (Math.abs(key - gap) <= tolerance) { + grouped.set(key, count + 1); + found = true; + break; + } + } + if (!found) { + grouped.set(gap, 1); + } + }); + + // Convert to tokens and sort by value + const tokens: SpacingToken[] = Array.from(grouped.entries()) + .map(([value, usage]) => ({ + value: Math.round(value), + usage, + suggestedName: this.suggestSpacingName(value) + })) + .sort((a, b) => a.value - b.value); + + return tokens; + } + + /** + * Suggest semantic name for spacing value + */ + private suggestSpacingName(value: number): string { + const rounded = Math.round(value); + if (rounded <= 4) return 'xs'; + if (rounded <= 8) return 'sm'; + if (rounded <= 12) return 'md'; + if (rounded <= 16) return 'base'; + if (rounded <= 24) return 'lg'; + if (rounded <= 32) return 'xl'; + if (rounded <= 48) return '2xl'; + if (rounded <= 64) return '3xl'; + return '4xl'; + } + + /** + * Extract typography styles from document + */ + extractTypographyStyles(doc: XDDocument): TypographyStyle[] { + const styles = new Map(); + + // Collect all text elements + const collectText = (elements: XDElement[]) => { + elements.forEach(el => { + if (el.type === 'text' && el.textStyle) { + const key = JSON.stringify({ + fontSize: el.textStyle.fontSize, + fontFamily: el.textStyle.fontFamily, + fontWeight: el.textStyle.fontWeight, + }); + + const existing = styles.get(key); + if (existing) { + existing.usage++; + } else { + styles.set(key, { + fontSize: el.textStyle.fontSize || 16, + fontFamily: el.textStyle.fontFamily, + fontWeight: el.textStyle.fontWeight, + lineHeight: el.textStyle.lineHeight, + letterSpacing: el.textStyle.letterSpacing, + color: el.textStyle.color, + usage: 1, + }); + } + } + + if (el.children) { + collectText(el.children); + } + }); + }; + + doc.artboards.forEach(artboard => collectText(artboard.elements)); + + // Convert to array and add suggested names + const typographyArray = Array.from(styles.values()) + .sort((a, b) => b.fontSize - a.fontSize) // Largest first + .map((style) => ({ + ...style, + suggestedName: this.suggestTypographyName(style.fontSize) + })); + + return typographyArray; + } + + /** + * Suggest semantic name for typography style + */ + private suggestTypographyName(fontSize: number): string { + if (fontSize >= 48) return 'display'; + if (fontSize >= 36) return 'h1'; + if (fontSize >= 30) return 'h2'; + if (fontSize >= 24) return 'h3'; + if (fontSize >= 20) return 'h4'; + if (fontSize >= 18) return 'h5'; + if (fontSize >= 16) return 'body'; + if (fontSize >= 14) return 'small'; + return 'caption'; + } + + /** + * Detect layout direction and patterns + */ + detectLayoutPattern(elements: XDElement[]): LayoutAnalysis { + if (elements.length === 0) { + return { + direction: 'vertical', + gaps: [], + padding: { top: 0, right: 0, bottom: 0, left: 0 }, + alignment: 'start', + suggestedCSS: { display: 'block' } + }; + } + + // Analyze element positions + const verticalGaps: number[] = []; + const horizontalGaps: number[] = []; + + const sortedByY = [...elements].sort((a, b) => (a.y || 0) - (b.y || 0)); + const sortedByX = [...elements].sort((a, b) => (a.x || 0) - (b.x || 0)); + + // Calculate gaps + for (let i = 0; i < sortedByY.length - 1; i++) { + const gap = (sortedByY[i + 1].y || 0) - ((sortedByY[i].y || 0) + (sortedByY[i].height || 0)); + if (gap > 0) verticalGaps.push(gap); + } + + for (let i = 0; i < sortedByX.length - 1; i++) { + const gap = (sortedByX[i + 1].x || 0) - ((sortedByX[i].x || 0) + (sortedByX[i].width || 0)); + if (gap > 0) horizontalGaps.push(gap); + } + + // Determine direction + const avgVerticalGap = verticalGaps.length > 0 ? + verticalGaps.reduce((a, b) => a + b, 0) / verticalGaps.length : 0; + const avgHorizontalGap = horizontalGaps.length > 0 ? + horizontalGaps.reduce((a, b) => a + b, 0) / horizontalGaps.length : 0; + + let direction: 'horizontal' | 'vertical' | 'mixed'; + if (horizontalGaps.length > verticalGaps.length * 1.5) { + direction = 'horizontal'; + } else if (verticalGaps.length > horizontalGaps.length * 1.5) { + direction = 'vertical'; + } else { + direction = 'mixed'; + } + + // Detect alignment + const xPositions = elements.map(el => el.x || 0); + const alignment = this.detectAlignment(xPositions); + + // Generate CSS suggestion + const suggestedCSS = this.generateLayoutCSS(direction, avgVerticalGap, avgHorizontalGap, alignment); + + return { + direction, + gaps: direction === 'horizontal' ? horizontalGaps : verticalGaps, + padding: { top: 0, right: 0, bottom: 0, left: 0 }, // TODO: Detect padding + alignment, + suggestedCSS + }; + } + + private detectAlignment(positions: number[]): 'start' | 'center' | 'end' | 'space-between' | 'space-around' { + // Simple heuristic - can be improved + const unique = new Set(positions).size; + if (unique === 1) return 'start'; + if (unique === positions.length) return 'space-between'; + return 'start'; + } + + private generateLayoutCSS( + direction: 'horizontal' | 'vertical' | 'mixed', + vGap: number, + hGap: number, + alignment: string + ) { + const css: any = { display: 'flex' }; + + if (direction === 'horizontal') { + css.flexDirection = 'row'; + if (hGap > 0) css.gap = `${Math.round(hGap)}px`; + } else if (direction === 'vertical') { + css.flexDirection = 'column'; + if (vGap > 0) css.gap = `${Math.round(vGap)}px`; + } + + if (alignment !== 'start') { + css.justifyContent = alignment; + } + + return css; + } + + /** + * Detect UI patterns (buttons, inputs, cards, etc.) + */ + detectUIPatterns(artboard: XDArtboard): UIPattern[] { + const patterns: UIPattern[] = []; + + // Look for button patterns (rectangle + text) + artboard.elements.forEach(el => { + if (el.type === 'rectangle' || el.type === 'group') { + const pattern = this.analyzeElementAsPattern(el); + if (pattern) { + patterns.push(pattern); + } + } + }); + + return patterns; + } + + private analyzeElementAsPattern(element: XDElement): UIPattern | null { + if (!element.width || !element.height) return null; + + const hasText = element.children?.some(child => child.type === 'text') || element.type === 'text'; + const textContent = this.getTextContent(element); + const childCount = element.children?.length || 0; + const aspectRatio = element.width / element.height; + + // Input field heuristic: rectangular with border, light background, possibly placeholder text + if (element.stroke && element.height >= 30 && element.height <= 60 && element.width > 100) { + // Look for light background (white/light gray) + const isLightBg = element.fill && ( + element.fill.includes('#fff') || + element.fill.includes('#faf') || + element.fill.includes('#f0f') || + element.fill.includes('#eee') + ); + + if (isLightBg || element.stroke) { + return { + type: 'input', + confidence: hasText ? 0.8 : 0.6, + elements: [element], + bounds: { + x: element.x || 0, + y: element.y || 0, + width: element.width, + height: element.height + }, + suggestedComponent: textContent?.toLowerCase().includes('email') ? 'EmailInput' : + textContent?.toLowerCase().includes('password') ? 'PasswordInput' : + textContent?.toLowerCase().includes('search') ? 'SearchInput' : 'TextInput' + }; + } + } + + // Button heuristic: rectangular with prominent background, contains text, clickable size + if (element.fill && hasText && element.height >= 30 && element.height <= 60) { + const isDarkBg = element.fill && !element.fill.includes('#fff') && !element.fill.includes('#faf'); + const isCompactButton = element.width >= 80 && element.width < 400 && aspectRatio > 1.5; + + if (isDarkBg && isCompactButton) { + return { + type: 'button', + confidence: 0.85, + elements: [element], + bounds: { + x: element.x || 0, + y: element.y || 0, + width: element.width, + height: element.height + }, + suggestedComponent: textContent?.toLowerCase().includes('submit') ? 'SubmitButton' : + textContent?.toLowerCase().includes('cancel') ? 'CancelButton' : + textContent?.toLowerCase().includes('save') ? 'SaveButton' : 'Button' + }; + } + } + + // Card heuristic: larger rectangle with multiple children, usually has shadow or border + if (childCount >= 3 && element.width > 200 && element.height > 150) { + const hasImageChild = element.children?.some(c => c.type === 'rectangle' && (c.width || 0) > 100); + + return { + type: 'card', + confidence: hasImageChild ? 0.75 : 0.6, + elements: [element], + bounds: { + x: element.x || 0, + y: element.y || 0, + width: element.width, + height: element.height + }, + suggestedComponent: hasImageChild ? 'ImageCard' : 'Card' + }; + } + + // Header/navigation heuristic: wide, horizontal, at top + if (element.width > 800 && element.height < 150 && (element.y || 0) < 100) { + return { + type: 'header', + confidence: childCount > 3 ? 0.7 : 0.5, + elements: [element], + bounds: { + x: element.x || 0, + y: element.y || 0, + width: element.width, + height: element.height + }, + suggestedComponent: 'Header' + }; + } + + // Footer heuristic: wide, horizontal, contains links/text + if (element.width > 800 && element.height < 200 && element.height > 60 && hasText) { + return { + type: 'footer', + confidence: 0.6, + elements: [element], + bounds: { + x: element.x || 0, + y: element.y || 0, + width: element.width, + height: element.height + }, + suggestedComponent: 'Footer' + }; + } + + return null; + } + + private getTextContent(element: XDElement): string { + if (element.text) return element.text; + if (element.children) { + for (const child of element.children) { + const text = this.getTextContent(child); + if (text) return text; + } + } + return ''; + } + + /** + * Extract border radius tokens from document + */ + extractBorderRadiusTokens(doc: XDDocument): BorderRadiusToken[] { + const radiusMap = new Map(); + + const collectRadius = (elements: XDElement[]) => { + elements.forEach(el => { + if (el.cornerRadius !== undefined && el.cornerRadius > 0) { + const rounded = Math.round(el.cornerRadius); + radiusMap.set(rounded, (radiusMap.get(rounded) || 0) + 1); + } + if (el.children) { + collectRadius(el.children); + } + }); + }; + + doc.artboards.forEach(artboard => collectRadius(artboard.elements)); + + return Array.from(radiusMap.entries()) + .map(([value, usage]) => ({ + value, + usage, + suggestedName: this.suggestRadiusName(value) + })) + .sort((a, b) => a.value - b.value); + } + + private suggestRadiusName(value: number): string { + if (value <= 2) return 'none'; + if (value <= 4) return 'sm'; + if (value <= 6) return 'md'; + if (value <= 8) return 'base'; + if (value <= 12) return 'lg'; + if (value <= 16) return 'xl'; + if (value <= 24) return '2xl'; + if (value >= 9999) return 'full'; // Fully rounded + return '3xl'; + } + + /** + * Extract shadow tokens from document + */ + extractShadowTokens(doc: XDDocument): ShadowToken[] { + const shadowsMap = new Map(); + + const collectShadows = (elements: XDElement[]) => { + elements.forEach(el => { + if (el.shadow) { + const key = `${el.shadow.x},${el.shadow.y},${el.shadow.blur},${el.shadow.color}`; + const existing = shadowsMap.get(key); + if (existing) { + existing.usage++; + } else { + shadowsMap.set(key, { shadow: el.shadow, usage: 1 }); + } + } + if (el.children) { + collectShadows(el.children); + } + }); + }; + + doc.artboards.forEach(artboard => collectShadows(artboard.elements)); + + return Array.from(shadowsMap.values()) + .map(({shadow, usage}, index) => ({ + x: shadow.x, + y: shadow.y, + blur: shadow.blur, + color: shadow.color, + usage, + cssShadow: `${shadow.x}px ${shadow.y}px ${shadow.blur}px ${shadow.color}`, + suggestedName: this.suggestShadowName(shadow.blur, index) + })) + .sort((a, b) => a.blur - b.blur); + } + + private suggestShadowName(blur: number, _index: number): string { + if (blur <= 2) return 'xs'; + if (blur <= 4) return 'sm'; + if (blur <= 8) return 'md'; + if (blur <= 12) return 'base'; + if (blur <= 16) return 'lg'; + if (blur <= 24) return 'xl'; + return '2xl'; + } + + /** + * Extract opacity tokens from document + */ + extractOpacityTokens(doc: XDDocument): OpacityToken[] { + const opacityMap = new Map(); + + const collectOpacity = (elements: XDElement[]) => { + elements.forEach(el => { + if (el.opacity !== undefined && el.opacity < 1) { + // Round to 2 decimal places + const rounded = Math.round(el.opacity * 100) / 100; + opacityMap.set(rounded, (opacityMap.get(rounded) || 0) + 1); + } + if (el.children) { + collectOpacity(el.children); + } + }); + }; + + doc.artboards.forEach(artboard => collectOpacity(artboard.elements)); + + return Array.from(opacityMap.entries()) + .map(([value, usage]) => ({ + value, + usage, + suggestedName: this.suggestOpacityName(value) + })) + .sort((a, b) => b.value - a.value); // Highest first + } + + private suggestOpacityName(value: number): string { + const percent = Math.round(value * 100); + if (percent >= 95) return 'high'; + if (percent >= 75) return 'medium'; + if (percent >= 50) return 'low'; + if (percent >= 25) return 'lower'; + return 'lowest'; + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index b1ef214..35eab74 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,7 +10,7 @@ import { XDTools } from './tools/xd-tools'; const server = new Server( { name: 'adobe-xd-mcp', - version: '1.0.0', + version: '2.0.0', }, { capabilities: { @@ -97,6 +97,155 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { required: ['path'], }, }, + { + name: 'get_artboard_details', + description: 'Get detailed information about a specific artboard including layout, positioning, and styling data', + inputSchema: { + type: 'object', + properties: { + path: { + type: 'string', + description: 'Path to the XD document', + }, + artboardName: { + type: 'string', + description: 'Name of the artboard to analyze', + }, + }, + required: ['path', 'artboardName'], + }, + }, + { + name: 'get_element_styles', + description: 'Get detailed styling information for elements in an artboard', + inputSchema: { + type: 'object', + properties: { + path: { + type: 'string', + description: 'Path to the XD document', + }, + artboardName: { + type: 'string', + description: 'Name of the artboard', + }, + elementName: { + type: 'string', + description: 'Name of specific element (optional - returns all elements if not specified)', + }, + }, + required: ['path', 'artboardName'], + }, + }, + { + name: 'export_artboard_preview', + description: 'Export an artboard preview as base64-encoded image for viewing in terminal', + inputSchema: { + type: 'object', + properties: { + path: { + type: 'string', + description: 'Path to the XD document', + }, + artboardName: { + type: 'string', + description: 'Name of the artboard to export', + }, + }, + required: ['path', 'artboardName'], + }, + }, + { + name: 'analyze_layout_spacing', + description: 'Analyze layout and spacing between elements in an artboard - critical for pixel-perfect implementation', + inputSchema: { + type: 'object', + properties: { + path: { + type: 'string', + description: 'Path to the XD document', + }, + artboardName: { + type: 'string', + description: 'Name of the artboard to analyze', + }, + }, + required: ['path', 'artboardName'], + }, + }, + { + name: 'generate_design_tokens', + description: 'Generate design tokens (colors, spacing, typography, borders) from XD document for consistent implementation', + inputSchema: { + type: 'object', + properties: { + path: { + type: 'string', + description: 'Path to the XD document', + }, + format: { + type: 'string', + enum: ['json', 'css', 'tailwind', 'typescript'], + description: 'Output format for design tokens (default: json)', + }, + outputFile: { + type: 'string', + description: 'File to save tokens to (optional)', + }, + }, + required: ['path'], + }, + }, + { + name: 'detect_ui_patterns', + description: 'Detect and identify common UI patterns (buttons, inputs, cards, etc.) in an artboard', + inputSchema: { + type: 'object', + properties: { + path: { + type: 'string', + description: 'Path to the XD document', + }, + artboardName: { + type: 'string', + description: 'Name of the artboard to analyze', + }, + }, + required: ['path', 'artboardName'], + }, + }, + { + name: 'extract_typography_system', + description: 'Extract and organize all typography styles into a reusable type system', + inputSchema: { + type: 'object', + properties: { + path: { + type: 'string', + description: 'Path to the XD document', + }, + }, + required: ['path'], + }, + }, + { + name: 'detect_layout_system', + description: 'Detect layout patterns (flexbox, grid) and suggest CSS implementation', + inputSchema: { + type: 'object', + properties: { + path: { + type: 'string', + description: 'Path to the XD document', + }, + artboardName: { + type: 'string', + description: 'Name of the artboard to analyze', + }, + }, + required: ['path', 'artboardName'], + }, + }, ], }; }); @@ -134,13 +283,94 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { if (!args || typeof args !== 'object' || !('path' in args)) { throw new Error('Invalid arguments: path is required'); } - result = await xdTools.extractColors(args as { - path: string; - format?: "tailwind" | "css" | "json"; + result = await xdTools.extractColors(args as { + path: string; + format?: "tailwind" | "css" | "json"; outputFile?: string; }); break; - + + case 'get_artboard_details': + if (!args || typeof args !== 'object' || !('path' in args) || !('artboardName' in args)) { + throw new Error('Invalid arguments: path and artboardName are required'); + } + result = await xdTools.getArtboardDetails(args as { + path: string; + artboardName: string; + }); + break; + + case 'get_element_styles': + if (!args || typeof args !== 'object' || !('path' in args) || !('artboardName' in args)) { + throw new Error('Invalid arguments: path and artboardName are required'); + } + result = await xdTools.getElementStyles(args as { + path: string; + artboardName: string; + elementName?: string; + }); + break; + + case 'export_artboard_preview': + if (!args || typeof args !== 'object' || !('path' in args) || !('artboardName' in args)) { + throw new Error('Invalid arguments: path and artboardName are required'); + } + result = await xdTools.exportArtboardPreview(args as { + path: string; + artboardName: string; + }); + break; + + case 'analyze_layout_spacing': + if (!args || typeof args !== 'object' || !('path' in args) || !('artboardName' in args)) { + throw new Error('Invalid arguments: path and artboardName are required'); + } + result = await xdTools.analyzeLayoutSpacing(args as { + path: string; + artboardName: string; + }); + break; + + case 'generate_design_tokens': + if (!args || typeof args !== 'object' || !('path' in args)) { + throw new Error('Invalid arguments: path is required'); + } + result = await xdTools.generateDesignTokens(args as { + path: string; + format?: 'json' | 'css' | 'tailwind' | 'typescript'; + outputFile?: string; + }); + break; + + case 'detect_ui_patterns': + if (!args || typeof args !== 'object' || !('path' in args) || !('artboardName' in args)) { + throw new Error('Invalid arguments: path and artboardName are required'); + } + result = await xdTools.detectUIPatterns(args as { + path: string; + artboardName: string; + }); + break; + + case 'extract_typography_system': + if (!args || typeof args !== 'object' || !('path' in args)) { + throw new Error('Invalid arguments: path is required'); + } + result = await xdTools.extractTypographySystem(args as { + path: string; + }); + break; + + case 'detect_layout_system': + if (!args || typeof args !== 'object' || !('path' in args) || !('artboardName' in args)) { + throw new Error('Invalid arguments: path and artboardName are required'); + } + result = await xdTools.detectLayoutSystem(args as { + path: string; + artboardName: string; + }); + break; + default: throw new Error(`Unknown tool: ${name}`); } diff --git a/src/parsers/xd-parser.ts b/src/parsers/xd-parser.ts index 6b4a1b9..25fcf6d 100644 --- a/src/parsers/xd-parser.ts +++ b/src/parsers/xd-parser.ts @@ -16,14 +16,33 @@ export interface XDElement { id: string; type: 'rectangle' | 'ellipse' | 'text' | 'group' | 'component'; name?: string; - x: number; - y: number; - width: number; - height: number; + x?: number; + y?: number; + width?: number; + height?: number; children?: XDElement[]; text?: string; - fill?: string; - stroke?: string; + fill?: any; + stroke?: any; + opacity?: number; + cornerRadius?: number; + shadow?: { + type: 'drop' | 'inner'; + x: number; + y: number; + blur: number; + color: string; + }; + textStyle?: { + fontSize?: number; + fontFamily?: string; + fontWeight?: number; + fontStyle?: string; + textAlign?: string; + lineHeight?: number; + letterSpacing?: number; + color?: string; + }; } export interface XDColor { @@ -48,30 +67,103 @@ export class XDParser { async parseDocument(filePath: string): Promise { const data = await fs.readFile(filePath); const zip = await JSZip.loadAsync(data); - + // Read manifest const manifestFile = zip.file('manifest'); if (!manifestFile) { throw new Error('Invalid XD file: no manifest found'); } - + const manifestContent = await manifestFile.async('string'); const manifest = JSON.parse(manifestContent); - - // Read artwork - const artworkFile = zip.file('artwork.agc'); - if (!artworkFile) { - throw new Error('Invalid XD file: no artwork found'); + + // Try to read old format (single artwork.agc file) + let artworkFile = zip.file('artwork.agc'); + + if (artworkFile) { + // Old format + const artworkContent = await artworkFile.async('string'); + const artwork = JSON.parse(artworkContent); + + const artboards = this.parseArtboards(artwork); + const colors = this.extractColors(artwork); + const components = this.parseComponents(artwork); + + return { + name: manifest.name || 'Untitled', + artboards, + colors, + components + }; + } else { + // New format - individual artboard files + return this.parseNewFormat(zip, manifest); } - - const artworkContent = await artworkFile.async('string'); - const artwork = JSON.parse(artworkContent); - - // Parse the document - const artboards = this.parseArtboards(artwork); - const colors = this.extractColors(artwork); - const components = this.parseComponents(artwork); - + } + + private async parseNewFormat(zip: JSZip, manifest: any): Promise { + const artboards: XDArtboard[] = []; + const colors: XDColor[] = []; + const colorSet = new Set(); + + // Find artwork section in manifest + const artworkSection = manifest.children?.find((child: any) => child.name === 'artwork'); + + if (!artworkSection || !artworkSection.children) { + throw new Error('Invalid XD file: no artwork found in manifest'); + } + + // Process each artboard + for (const child of artworkSection.children) { + // Skip pasteboard + if (child.name === 'pasteboard') continue; + + // This is an artboard + if (child.path && child.children) { + const graphicsChild = child.children.find((c: any) => c.name === 'graphics'); + if (!graphicsChild || !graphicsChild.components) continue; + + const graphicComponent = graphicsChild.components.find((c: any) => + c.type === 'application/vnd.adobe.agc.graphicsTree+json' + ); + + if (!graphicComponent) continue; + + // Read the graphics file + const graphicsPath = `artwork/${child.path}/graphics/${graphicComponent.path}`; + const graphicsFile = zip.file(graphicsPath); + + if (!graphicsFile) continue; + + const graphicsContent = await graphicsFile.async('string'); + const graphics = JSON.parse(graphicsContent); + + // Extract artboard data + const artboardData = graphics.children?.[0]; // First child is the artboard + + if (artboardData && artboardData.type === 'artboard') { + const bounds = child['uxdesign#bounds'] || {}; + const elements = this.parseElements(artboardData.artboard?.children || []); + + artboards.push({ + id: child.id, + name: child.name || 'Untitled Artboard', + x: bounds.x || 0, + y: bounds.y || 0, + width: bounds.width || 0, + height: bounds.height || 0, + elements + }); + + // Extract colors from this artboard + this.extractColorsFromObject(artboardData, colors, colorSet); + } + } + } + + // Parse components from resources + const components = await this.parseComponentsNewFormat(zip, manifest); + return { name: manifest.name || 'Untitled', artboards, @@ -79,6 +171,82 @@ export class XDParser { components }; } + + private async parseComponentsNewFormat(zip: JSZip, manifest: any): Promise { + const components: XDComponent[] = []; + + // Find resources section + const resourcesSection = manifest.children?.find((child: any) => child.name === 'resources'); + + if (!resourcesSection) return components; + + // Check if there's a graphics section with components + const graphicsChild = resourcesSection.children?.find((c: any) => c.name === 'graphics'); + + if (graphicsChild?.components) { + const graphicComponent = graphicsChild.components.find((c: any) => + c.type === 'application/vnd.adobe.agc.graphicsTree+json' + ); + + if (graphicComponent) { + const graphicsPath = `resources/graphics/${graphicComponent.path}`; + const graphicsFile = zip.file(graphicsPath); + + if (graphicsFile) { + const graphicsContent = await graphicsFile.async('string'); + const graphics = JSON.parse(graphicsContent); + + // Extract components from resources + if (graphics.resources?.clipboardGraphics) { + for (const [id, component] of Object.entries(graphics.resources.clipboardGraphics)) { + components.push({ + id, + name: (component as any).name || 'Untitled Component', + elements: this.parseElements((component as any).artboard?.children || []) + }); + } + } + } + } + } + + return components; + } + + private extractColorsFromObject(obj: any, colors: XDColor[], colorSet: Set): void { + if (!obj || typeof obj !== 'object') return; + + // Check for fill colors + if (obj.style?.fill?.color) { + const hex = this.colorToHex(obj.style.fill.color); + if (!colorSet.has(hex)) { + colorSet.add(hex); + colors.push({ value: hex }); + } + } + + // Check for stroke colors + if (obj.style?.stroke?.color) { + const hex = this.colorToHex(obj.style.stroke.color); + if (!colorSet.has(hex)) { + colorSet.add(hex); + colors.push({ value: hex }); + } + } + + // Recurse through children + if (obj.artboard?.children) { + for (const child of obj.artboard.children) { + this.extractColorsFromObject(child, colors, colorSet); + } + } + + if (obj.shape?.children) { + for (const child of obj.shape.children) { + this.extractColorsFromObject(child, colors, colorSet); + } + } + } private parseArtboards(artwork: any): XDArtboard[] { const artboards: XDArtboard[] = []; @@ -104,39 +272,110 @@ export class XDParser { private parseElements(children: any[]): XDElement[] { const elements: XDElement[] = []; - + for (const child of children) { + // Get dimensions from shape property if available (new format) + const width = child.shape?.width || child.width || 0; + const height = child.shape?.height || child.height || 0; + const element: XDElement = { id: child.id, type: this.mapElementType(child.type), name: child.name, x: child.transform?.tx || 0, y: child.transform?.ty || 0, - width: child.width || 0, - height: child.height || 0 + width, + height }; - + // Add type-specific properties if (child.type === 'text') { element.text = child.text?.rawText || ''; + + // Extract text styling from various possible locations + const textStyle: any = {}; + + // Try to get font info from style.font or text.frame.paragraphs + if (child.style?.font) { + textStyle.fontSize = child.style.font.size; + textStyle.fontFamily = child.style.font.family; + textStyle.fontWeight = child.style.font.weight; + textStyle.fontStyle = child.style.font.style; + } + + // Also check text.frame for styling + if (child.text?.frame) { + const para = child.text.frame.paragraphs?.[0]; + if (para) { + textStyle.fontSize = textStyle.fontSize || para.fontSize; + textStyle.fontFamily = textStyle.fontFamily || para.fontFamily; + textStyle.fontWeight = textStyle.fontWeight || para.fontWeight; + textStyle.textAlign = para.textAlign; + textStyle.lineHeight = para.lineHeight; + textStyle.letterSpacing = para.letterSpacing; + } + } + + // Get text color + if (child.style?.fill?.color) { + textStyle.color = this.colorToHex(child.style.fill.color); + } + + // Only add textStyle if we found any properties + if (Object.keys(textStyle).length > 0) { + element.textStyle = textStyle; + } } - - if (child.fill?.color) { + + // Handle style colors (new format uses style property) + if (child.style?.fill?.color) { + element.fill = this.colorToHex(child.style.fill.color); + } else if (child.fill?.color) { element.fill = this.colorToHex(child.fill.color); } - - if (child.stroke?.color) { + + if (child.style?.stroke?.color) { + element.stroke = this.colorToHex(child.style.stroke.color); + } else if (child.stroke?.color) { element.stroke = this.colorToHex(child.stroke.color); } - + + // Opacity + if (child.style?.opacity !== undefined) { + element.opacity = child.style.opacity; + } else if (child.opacity !== undefined) { + element.opacity = child.opacity; + } + + // Corner radius + if (child.shape?.cornerRadius !== undefined) { + element.cornerRadius = child.shape.cornerRadius; + } else if (child.cornerRadius !== undefined) { + element.cornerRadius = child.cornerRadius; + } + + // Shadow/drop shadow + if (child.style?.filters) { + const dropShadow = child.style.filters.find((f: any) => f.type === 'dropShadow'); + if (dropShadow && dropShadow.params) { + element.shadow = { + type: 'drop', + x: dropShadow.params.dx || 0, + y: dropShadow.params.dy || 0, + blur: dropShadow.params.r || 0, + color: dropShadow.params.color ? this.colorToHex(dropShadow.params.color) : '#000000' + }; + } + } + // Parse children recursively if (child.children && child.children.length > 0) { element.children = this.parseElements(child.children); } - + elements.push(element); } - + return elements; } @@ -207,14 +446,50 @@ export class XDParser { private colorToHex(color: any): string { if (!color) return '#000000'; - + + // Handle new format with value property containing RGB values (0-255) + if (color.value) { + const r = color.value.r || 0; + const g = color.value.g || 0; + const b = color.value.b || 0; + + return '#' + [r, g, b].map((x: number) => { + const hex = x.toString(16); + return hex.length === 1 ? '0' + hex : hex; + }).join(''); + } + + // Handle old format with direct r, g, b properties (0-1 range) const r = Math.round((color.r || 0) * 255); const g = Math.round((color.g || 0) * 255); const b = Math.round((color.b || 0) * 255); - + return '#' + [r, g, b].map(x => { const hex = x.toString(16); return hex.length === 1 ? '0' + hex : hex; }).join(''); } + + async extractThumbnail(filePath: string, artboardId: string): Promise { + try { + const data = await fs.readFile(filePath); + const zip = await JSZip.loadAsync(data); + + // Check for thumbnail in renditions folder + const thumbnailPath = `resources/renditions/${artboardId}.png`; + const thumbnailFile = zip.file(thumbnailPath); + + if (thumbnailFile) { + const thumbnailData = await thumbnailFile.async('nodebuffer'); + const tempPath = `/tmp/xd-thumbnail-${artboardId}.png`; + await fs.writeFile(tempPath, thumbnailData); + return tempPath; + } + + return null; + } catch (error) { + console.error('Error extracting thumbnail:', error); + return null; + } + } } diff --git a/src/test-features.ts b/src/test-features.ts new file mode 100644 index 0000000..cf00fec --- /dev/null +++ b/src/test-features.ts @@ -0,0 +1,247 @@ +// src/test-features.ts +import { XDTools } from './tools/xd-tools'; + +const xdTools = new XDTools(); + +// Get file path from command line arguments +const args = process.argv.slice(2); +const testFilePath = args[0]; + +if (!testFilePath) { + console.error('โŒ Error: Please provide an XD file path'); + console.error('\nUsage: node dist/test-features.js [artboard-name]'); + console.error('\nExample: node dist/test-features.js "/path/to/design.xd" "Homepage"'); + process.exit(1); +} + +const targetArtboard = args[1] || null; + +async function runTests() { + console.log('๐Ÿงช Testing Adobe XD MCP Features\n'); + console.log(`๐Ÿ“ File: ${testFilePath}`); + if (targetArtboard) { + console.log(`๐ŸŽฏ Target Artboard: ${targetArtboard}`); + } + console.log('=' .repeat(60)); + + // Test 1: Get XD Info + console.log('\n๐Ÿ“‹ Test 1: Get XD Info'); + console.log('-'.repeat(60)); + try { + const info = await xdTools.getDocumentInfo({ path: testFilePath }); + console.log('โœ… Success!'); + console.log(`Document: ${info.info?.name}`); + console.log(`Artboards: ${info.info?.artboardCount}`); + console.log(`Colors: ${info.info?.colorCount}`); + if (info.info?.artboards) { + console.log('\nArtboards:'); + info.info.artboards.forEach((a: any) => { + console.log(` - ${a.name} (${a.width}x${a.height}, ${a.elementCount} elements)`); + }); + } + } catch (error) { + console.error('โŒ Error:', error); + } + + // First, get document info to find available artboards + let firstArtboardName: string | null = null; + try { + const info = await xdTools.getDocumentInfo({ path: testFilePath }); + if (info.info?.artboards && info.info.artboards.length > 0) { + firstArtboardName = targetArtboard || info.info.artboards[0].name; + } + } catch (error) { + console.error('โŒ Error getting artboard list:', error); + } + + if (!firstArtboardName) { + console.log('\nโŒ No artboards found in document. Skipping artboard-specific tests.\n'); + return; + } + + // Test 2: Get Artboard Details + console.log('\n\n๐Ÿ“ Test 2: Get Artboard Details'); + console.log('-'.repeat(60)); + try { + const details = await xdTools.getArtboardDetails({ + path: testFilePath, + artboardName: firstArtboardName + }); + console.log('โœ… Success!'); + if (details.artboard) { + console.log(`Artboard: ${details.artboard.name}`); + console.log(`Size: ${details.artboard.width}x${details.artboard.height}`); + console.log(`Elements: ${details.artboard.elementCount}`); + console.log('\nFirst 5 elements:'); + details.artboard.elements.slice(0, 5).forEach((el: any) => { + console.log(` - ${el.type}: ${el.name || 'unnamed'} at (${el.position.x}, ${el.position.y})`); + }); + } + } catch (error) { + console.error('โŒ Error:', error); + } + + // Test 3: Analyze Layout Spacing + console.log('\n\n๐Ÿ“ Test 3: Analyze Layout Spacing'); + console.log('-'.repeat(60)); + try { + const spacing = await xdTools.analyzeLayoutSpacing({ + path: testFilePath, + artboardName: firstArtboardName + }); + console.log('โœ… Success!'); + if (spacing.spacingTokens) { + console.log('\nSpacing Tokens:'); + spacing.spacingTokens.forEach((token: any) => { + console.log(` - ${token.suggestedName}: ${token.value}px (used ${token.usage} times)`); + }); + } + if (spacing.layoutAnalysis) { + console.log('\nLayout Analysis:'); + console.log(` Direction: ${spacing.layoutAnalysis.direction}`); + console.log(` Suggested CSS:`, JSON.stringify(spacing.layoutAnalysis.suggestedCSS, null, 2)); + } + } catch (error) { + console.error('โŒ Error:', error); + } + + // Test 4: Generate Design Tokens + console.log('\n\n๐ŸŽจ Test 4: Generate Design Tokens (JSON)'); + console.log('-'.repeat(60)); + try { + const tokens = await xdTools.generateDesignTokens({ + path: testFilePath, + format: 'json' + }); + console.log('โœ… Success!'); + if (tokens.tokens) { + console.log(`\nSpacing tokens: ${tokens.tokens.spacing.length}`); + console.log(`Color tokens: ${tokens.tokens.colors.length}`); + console.log(`Typography styles: ${tokens.tokens.typography.length}`); + + console.log('\nTop 3 spacing values:'); + tokens.tokens.spacing.slice(0, 3).forEach((t: any) => { + console.log(` - ${t.suggestedName}: ${t.value}px`); + }); + + console.log('\nTop 3 colors:'); + tokens.tokens.colors.slice(0, 3).forEach((c: any) => { + console.log(` - ${c.value} (used ${c.usage} times)`); + }); + } + } catch (error) { + console.error('โŒ Error:', error); + } + + // Test 5: Detect UI Patterns + console.log('\n\n๐Ÿ” Test 5: Detect UI Patterns'); + console.log('-'.repeat(60)); + try { + const patterns = await xdTools.detectUIPatterns({ + path: testFilePath, + artboardName: firstArtboardName + }); + console.log('โœ… Success!'); + console.log(`\nDetected ${patterns.patternCount} patterns`); + if (patterns.patterns && patterns.patterns.length > 0) { + patterns.patterns.forEach((p: any, i: number) => { + console.log(`\n Pattern ${i + 1}:`); + console.log(` Type: ${p.type}`); + console.log(` Confidence: ${(p.confidence * 100).toFixed(0)}%`); + console.log(` Component: ${p.suggestedComponent}`); + console.log(` Bounds: ${p.bounds.width}x${p.bounds.height} at (${p.bounds.x}, ${p.bounds.y})`); + }); + } else { + console.log(' No patterns detected in this artboard'); + } + } catch (error) { + console.error('โŒ Error:', error); + } + + // Test 6: Extract Typography System + console.log('\n\n๐Ÿ“ Test 6: Extract Typography System'); + console.log('-'.repeat(60)); + try { + const typography = await xdTools.extractTypographySystem({ + path: testFilePath + }); + console.log('โœ… Success!'); + console.log(`\nFound ${typography.styleCount} typography styles`); + if (typography.styles && typography.styles.length > 0) { + console.log('\nTop typography styles:'); + typography.styles.slice(0, 5).forEach((style: any) => { + console.log(` - ${style.suggestedName}: ${style.fontSize}px, weight ${style.fontWeight || 400} (used ${style.usage} times)`); + }); + } + } catch (error) { + console.error('โŒ Error:', error); + } + + // Test 7: Detect Layout System + console.log('\n\n๐Ÿ—๏ธ Test 7: Detect Layout System'); + console.log('-'.repeat(60)); + try { + const layout = await xdTools.detectLayoutSystem({ + path: testFilePath, + artboardName: firstArtboardName + }); + console.log('โœ… Success!'); + if (layout.layout) { + console.log(`\nLayout Direction: ${layout.layout.direction}`); + console.log(`Gaps detected: ${layout.layout.gaps.length}`); + console.log('\nSuggested CSS:'); + Object.entries(layout.layout.suggestedCSS).forEach(([key, value]) => { + console.log(` ${key}: ${value}`); + }); + } + } catch (error) { + console.error('โŒ Error:', error); + } + + // Test 8: Get Element Styles + console.log('\n\n๐ŸŽญ Test 8: Get Element Styles'); + console.log('-'.repeat(60)); + try { + const styles = await xdTools.getElementStyles({ + path: testFilePath, + artboardName: firstArtboardName + }); + console.log('โœ… Success!'); + console.log(`\nFound styles for ${styles.elementCount} elements`); + if (styles.elements && styles.elements.length > 0) { + console.log('\nFirst element with styles:'); + const firstElement = styles.elements.find((el: any) => Object.keys(el.css).length > 2); + if (firstElement) { + console.log(` Name: ${firstElement.name || 'unnamed'}`); + console.log(` Type: ${firstElement.type}`); + console.log(` CSS:`, JSON.stringify(firstElement.css, null, 4)); + console.log(` Tailwind: ${firstElement.tailwindClasses.join(' ')}`); + } + } + } catch (error) { + console.error('โŒ Error:', error); + } + + // Test 9: Extract Colors + console.log('\n\n๐ŸŒˆ Test 9: Extract Colors'); + console.log('-'.repeat(60)); + try { + const colors = await xdTools.extractColors({ + path: testFilePath, + format: 'css' + }); + console.log('โœ… Success!'); + console.log(`\nFound ${colors.colorCount} colors`); + console.log('\nCSS Variables:'); + console.log(colors.output?.split('\n').slice(0, 8).join('\n')); + console.log(' ...'); + } catch (error) { + console.error('โŒ Error:', error); + } + + console.log('\n' + '='.repeat(60)); + console.log('โœ… All tests completed!\n'); +} + +// Run tests +runTests().catch(console.error); \ No newline at end of file diff --git a/src/tools/xd-tools.ts b/src/tools/xd-tools.ts index 88e56dc..a3954ec 100644 --- a/src/tools/xd-tools.ts +++ b/src/tools/xd-tools.ts @@ -3,6 +3,7 @@ import * as fs from 'fs/promises'; import * as path from 'path'; import { XDParser } from '../parsers/xd-parser'; import { ReactGenerator, GeneratorOptions } from '../generators/react-generator'; +import { DesignSystemAnalyzer } from '../analyzers/design-system-analyzer'; export class XDTools { private parser: XDParser; @@ -251,4 +252,621 @@ export class XDTools { .replace(/[\s_]+/g, '-') .toLowerCase(); } + + async getArtboardDetails(args: { path: string; artboardName: string }) { + try { + const doc = await this.parser.parseDocument(args.path); + const artboard = doc.artboards.find( + (a) => a.name.toLowerCase() === args.artboardName.toLowerCase() + ); + + if (!artboard) { + return { + success: false, + error: `Artboard "${args.artboardName}" not found`, + }; + } + + // Extract detailed element information with positioning and styling + const detailedElements = artboard.elements.map((el) => ({ + id: el.id, + name: el.name, + type: el.type, + position: { + x: el.x || 0, + y: el.y || 0, + }, + size: { + width: el.width || 0, + height: el.height || 0, + }, + style: { + fill: el.fill, + stroke: el.stroke, + opacity: el.opacity, + cornerRadius: el.cornerRadius, + }, + text: el.text, + textStyle: el.textStyle + ? { + fontSize: el.textStyle.fontSize, + fontFamily: el.textStyle.fontFamily, + fontWeight: el.textStyle.fontWeight, + fontStyle: el.textStyle.fontStyle, + textAlign: el.textStyle.textAlign, + lineHeight: el.textStyle.lineHeight, + letterSpacing: el.textStyle.letterSpacing, + } + : undefined, + hasChildren: (el.children?.length || 0) > 0, + childCount: el.children?.length || 0, + })); + + return { + success: true, + artboard: { + id: artboard.id, + name: artboard.name, + width: artboard.width, + height: artboard.height, + elementCount: artboard.elements.length, + elements: detailedElements, + }, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + } + + async getElementStyles(args: { + path: string; + artboardName: string; + elementName?: string; + }) { + try { + const doc = await this.parser.parseDocument(args.path); + const artboard = doc.artboards.find( + (a) => a.name.toLowerCase() === args.artboardName.toLowerCase() + ); + + if (!artboard) { + return { + success: false, + error: `Artboard "${args.artboardName}" not found`, + }; + } + + let elements = artboard.elements; + + if (args.elementName) { + elements = elements.filter( + (el) => el.name?.toLowerCase() === args.elementName!.toLowerCase() + ); + + if (elements.length === 0) { + return { + success: false, + error: `Element "${args.elementName}" not found in artboard "${args.artboardName}"`, + }; + } + } + + const styledElements = elements.map((el) => { + const cssProperties: Record = {}; + + // Position + if (el.x !== undefined) cssProperties.left = `${el.x}px`; + if (el.y !== undefined) cssProperties.top = `${el.y}px`; + + // Size + if (el.width !== undefined) cssProperties.width = `${el.width}px`; + if (el.height !== undefined) cssProperties.height = `${el.height}px`; + + // Fill/Background + if (el.fill) { + cssProperties.backgroundColor = el.fill.value || el.fill; + } + + // Stroke/Border + if (el.stroke) { + cssProperties.borderColor = el.stroke.value || el.stroke; + cssProperties.borderWidth = '1px'; + cssProperties.borderStyle = 'solid'; + } + + // Opacity + if (el.opacity !== undefined && el.opacity < 1) { + cssProperties.opacity = el.opacity.toString(); + } + + // Border radius + if (el.cornerRadius !== undefined && el.cornerRadius > 0) { + cssProperties.borderRadius = `${el.cornerRadius}px`; + } + + // Text styles + if (el.textStyle) { + if (el.textStyle.fontSize) + cssProperties.fontSize = `${el.textStyle.fontSize}px`; + if (el.textStyle.fontFamily) + cssProperties.fontFamily = el.textStyle.fontFamily; + if (el.textStyle.fontWeight) + cssProperties.fontWeight = el.textStyle.fontWeight.toString(); + if (el.textStyle.fontStyle) + cssProperties.fontStyle = el.textStyle.fontStyle; + if (el.textStyle.textAlign) + cssProperties.textAlign = el.textStyle.textAlign; + if (el.textStyle.lineHeight) + cssProperties.lineHeight = el.textStyle.lineHeight.toString(); + if (el.textStyle.letterSpacing) + cssProperties.letterSpacing = `${el.textStyle.letterSpacing}px`; + if (el.textStyle.color) + cssProperties.color = el.textStyle.color; + } + + return { + name: el.name, + type: el.type, + css: cssProperties, + tailwindClasses: this.generateTailwindFromStyles(cssProperties), + }; + }); + + return { + success: true, + artboardName: args.artboardName, + elementCount: styledElements.length, + elements: styledElements, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + } + + async exportArtboardPreview(args: { path: string; artboardName: string }) { + try { + const doc = await this.parser.parseDocument(args.path); + const artboard = doc.artboards.find( + (a) => a.name.toLowerCase() === args.artboardName.toLowerCase() + ); + + if (!artboard) { + return { + success: false, + error: `Artboard "${args.artboardName}" not found`, + }; + } + + // Check if XD file has thumbnails + const thumbnailPath = await this.parser.extractThumbnail( + args.path, + artboard.id + ); + + if (!thumbnailPath) { + return { + success: false, + error: + 'Preview generation not supported - XD file does not contain renditions', + }; + } + + // Read thumbnail and convert to base64 + const thumbnailData = await fs.readFile(thumbnailPath); + const base64Image = thumbnailData.toString('base64'); + + return { + success: true, + artboardName: artboard.name, + format: 'png', + base64: base64Image, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + } + + private generateTailwindFromStyles(css: Record): string[] { + const classes: string[] = []; + + // Background color + if (css.backgroundColor) { + classes.push(`bg-[${css.backgroundColor}]`); + } + + // Text color + if (css.color) { + classes.push(`text-[${css.color}]`); + } + + // Font size + if (css.fontSize) { + const size = parseInt(css.fontSize); + if (size <= 12) classes.push('text-xs'); + else if (size <= 14) classes.push('text-sm'); + else if (size <= 16) classes.push('text-base'); + else if (size <= 18) classes.push('text-lg'); + else if (size <= 20) classes.push('text-xl'); + else if (size <= 24) classes.push('text-2xl'); + else classes.push('text-3xl'); + } + + // Font weight + if (css.fontWeight) { + const weight = parseInt(css.fontWeight); + if (weight <= 300) classes.push('font-light'); + else if (weight <= 400) classes.push('font-normal'); + else if (weight <= 500) classes.push('font-medium'); + else if (weight <= 600) classes.push('font-semibold'); + else classes.push('font-bold'); + } + + // Border radius + if (css.borderRadius) { + const radius = parseInt(css.borderRadius); + if (radius <= 4) classes.push('rounded'); + else if (radius <= 8) classes.push('rounded-lg'); + else classes.push('rounded-xl'); + } + + // Opacity + if (css.opacity) { + const opacity = parseFloat(css.opacity) * 100; + classes.push(`opacity-${Math.round(opacity)}`); + } + + return classes; + } + + async analyzeLayoutSpacing(args: { path: string; artboardName: string }) { + try { + const doc = await this.parser.parseDocument(args.path); + const artboard = doc.artboards.find( + (a) => a.name.toLowerCase() === args.artboardName.toLowerCase() + ); + + if (!artboard) { + return { + success: false, + error: `Artboard "${args.artboardName}" not found`, + }; + } + + const analyzer = new DesignSystemAnalyzer(); + + // Analyze spacing + const gaps = analyzer.analyzeSpacing(artboard.elements); + const spacingTokens = analyzer.extractSpacingTokens(gaps); + + // Detect layout patterns + const layoutAnalysis = analyzer.detectLayoutPattern(artboard.elements); + + return { + success: true, + artboardName: artboard.name, + spacingTokens, + layoutAnalysis, + elementCount: artboard.elements.length, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + } + + async generateDesignTokens(args: { + path: string; + format?: 'json' | 'css' | 'tailwind' | 'typescript'; + outputFile?: string; + }) { + try { + const doc = await this.parser.parseDocument(args.path); + const analyzer = new DesignSystemAnalyzer(); + + // Collect all gaps from all artboards + const allGaps: number[] = []; + doc.artboards.forEach((artboard) => { + const gaps = analyzer.analyzeSpacing(artboard.elements); + allGaps.push(...gaps); + }); + + const spacingTokens = analyzer.extractSpacingTokens(allGaps); + const typographyStyles = analyzer.extractTypographyStyles(doc); + + // Count color usage + const colorUsage = new Map(); + doc.colors.forEach((color) => { + colorUsage.set(color.value, (colorUsage.get(color.value) || 0) + 1); + }); + + const tokens = { + spacing: spacingTokens, + colors: doc.colors.map((c) => ({ + value: c.value, + usage: colorUsage.get(c.value) || 1, + suggestedName: c.name, + })), + typography: typographyStyles, + borderRadius: analyzer.extractBorderRadiusTokens(doc), + shadows: analyzer.extractShadowTokens(doc), + opacity: analyzer.extractOpacityTokens(doc), + }; + + const format = args.format || 'json'; + let output: string; + + switch (format) { + case 'json': + output = JSON.stringify(tokens, null, 2); + break; + case 'css': + output = this.generateCSSTokens(tokens); + break; + case 'tailwind': + output = this.generateTailwindTokens(tokens); + break; + case 'typescript': + output = this.generateTypeScriptTokens(tokens); + break; + default: + output = JSON.stringify(tokens, null, 2); + } + + if (args.outputFile) { + await fs.writeFile(args.outputFile, output, 'utf-8'); + } + + return { + success: true, + format, + tokens, + output: args.outputFile ? `Written to ${args.outputFile}` : output, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + } + + async detectUIPatterns(args: { path: string; artboardName: string }) { + try { + const doc = await this.parser.parseDocument(args.path); + const artboard = doc.artboards.find( + (a) => a.name.toLowerCase() === args.artboardName.toLowerCase() + ); + + if (!artboard) { + return { + success: false, + error: `Artboard "${args.artboardName}" not found`, + }; + } + + const analyzer = new DesignSystemAnalyzer(); + const patterns = analyzer.detectUIPatterns(artboard); + + return { + success: true, + artboardName: artboard.name, + patternCount: patterns.length, + patterns: patterns.map((p) => ({ + type: p.type, + confidence: p.confidence, + bounds: p.bounds, + suggestedComponent: p.suggestedComponent, + })), + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + } + + async extractTypographySystem(args: { path: string }) { + try { + const doc = await this.parser.parseDocument(args.path); + const analyzer = new DesignSystemAnalyzer(); + const typographyStyles = analyzer.extractTypographyStyles(doc); + + return { + success: true, + styleCount: typographyStyles.length, + styles: typographyStyles, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + } + + async detectLayoutSystem(args: { path: string; artboardName: string }) { + try { + const doc = await this.parser.parseDocument(args.path); + const artboard = doc.artboards.find( + (a) => a.name.toLowerCase() === args.artboardName.toLowerCase() + ); + + if (!artboard) { + return { + success: false, + error: `Artboard "${args.artboardName}" not found`, + }; + } + + const analyzer = new DesignSystemAnalyzer(); + const layoutAnalysis = analyzer.detectLayoutPattern(artboard.elements); + + return { + success: true, + artboardName: artboard.name, + layout: layoutAnalysis, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + } + + private generateCSSTokens(tokens: any): string { + const lines = [':root {', ' /* Spacing tokens */']; + + tokens.spacing.forEach((token: any) => { + lines.push(` --spacing-${token.suggestedName}: ${token.value}px;`); + }); + + lines.push('', ' /* Color tokens */'); + tokens.colors.forEach((color: any, index: number) => { + const name = color.suggestedName || `color-${index + 1}`; + lines.push(` --${this.toKebabCase(name)}: ${color.value};`); + }); + + lines.push('', ' /* Typography tokens */'); + tokens.typography.forEach((style: any) => { + const name = style.suggestedName || 'text'; + lines.push( + ` --font-${name}: ${style.fontWeight || 400} ${style.fontSize}px/${style.lineHeight || 1.5} ${style.fontFamily || 'sans-serif'};` + ); + }); + + lines.push('', ' /* Border radius tokens */'); + tokens.borderRadius.forEach((token: any) => { + lines.push(` --radius-${token.suggestedName}: ${token.value}px;`); + }); + + lines.push('', ' /* Shadow tokens */'); + tokens.shadows.forEach((token: any) => { + lines.push(` --shadow-${token.suggestedName}: ${token.cssShadow};`); + }); + + lines.push('', ' /* Opacity tokens */'); + tokens.opacity.forEach((token: any) => { + lines.push(` --opacity-${token.suggestedName}: ${token.value};`); + }); + + lines.push('}'); + return lines.join('\n'); + } + + private generateTailwindTokens(tokens: any): string { + const spacing: any = {}; + tokens.spacing.forEach((token: any) => { + spacing[token.suggestedName] = `${token.value}px`; + }); + + const colors: any = {}; + tokens.colors.forEach((color: any, index: number) => { + const name = color.suggestedName || `color${index + 1}`; + colors[this.toCamelCase(name)] = color.value; + }); + + const fontSize: any = {}; + tokens.typography.forEach((style: any) => { + fontSize[style.suggestedName] = [ + `${style.fontSize}px`, + { lineHeight: style.lineHeight?.toString() || '1.5' }, + ]; + }); + + const borderRadius: any = {}; + tokens.borderRadius.forEach((token: any) => { + borderRadius[token.suggestedName] = `${token.value}px`; + }); + + const boxShadow: any = {}; + tokens.shadows.forEach((token: any) => { + boxShadow[token.suggestedName] = token.cssShadow; + }); + + const opacity: any = {}; + tokens.opacity.forEach((token: any) => { + opacity[token.suggestedName] = token.value.toString(); + }); + + return `module.exports = { + theme: { + extend: { + spacing: ${JSON.stringify(spacing, null, 6).replace(/\n/g, '\n ')}, + colors: ${JSON.stringify(colors, null, 6).replace(/\n/g, '\n ')}, + fontSize: ${JSON.stringify(fontSize, null, 6).replace(/\n/g, '\n ')}, + borderRadius: ${JSON.stringify(borderRadius, null, 6).replace(/\n/g, '\n ')}, + boxShadow: ${JSON.stringify(boxShadow, null, 6).replace(/\n/g, '\n ')}, + opacity: ${JSON.stringify(opacity, null, 6).replace(/\n/g, '\n ')} + } + } +}`; + } + + private generateTypeScriptTokens(tokens: any): string { + const lines = ['export const designTokens = {', ' spacing: {']; + + tokens.spacing.forEach((token: any, index: number) => { + const comma = index < tokens.spacing.length - 1 ? ',' : ''; + lines.push(` ${token.suggestedName}: ${token.value}${comma}`); + }); + + lines.push(' },', ' colors: {'); + + tokens.colors.forEach((color: any, index: number) => { + const name = color.suggestedName || `color${index + 1}`; + const comma = index < tokens.colors.length - 1 ? ',' : ''; + lines.push(` ${this.toCamelCase(name)}: '${color.value}'${comma}`); + }); + + lines.push(' },', ' typography: {'); + + tokens.typography.forEach((style: any, index: number) => { + const comma = index < tokens.typography.length - 1 ? ',' : ''; + lines.push(` ${style.suggestedName}: {`); + lines.push(` fontSize: ${style.fontSize},`); + if (style.fontFamily) + lines.push(` fontFamily: '${style.fontFamily}',`); + if (style.fontWeight) lines.push(` fontWeight: ${style.fontWeight},`); + if (style.lineHeight) lines.push(` lineHeight: ${style.lineHeight},`); + lines.push(` }${comma}`); + }); + + lines.push(' },', ' borderRadius: {'); + + tokens.borderRadius.forEach((token: any, index: number) => { + const comma = index < tokens.borderRadius.length - 1 ? ',' : ''; + lines.push(` ${token.suggestedName}: ${token.value}${comma}`); + }); + + lines.push(' },', ' shadows: {'); + + tokens.shadows.forEach((token: any, index: number) => { + const comma = index < tokens.shadows.length - 1 ? ',' : ''; + lines.push(` ${token.suggestedName}: '${token.cssShadow}'${comma}`); + }); + + lines.push(' },', ' opacity: {'); + + tokens.opacity.forEach((token: any, index: number) => { + const comma = index < tokens.opacity.length - 1 ? ',' : ''; + lines.push(` ${token.suggestedName}: ${token.value}${comma}`); + }); + + lines.push(' }', '} as const;'); + + return lines.join('\n'); + } }